最佳化準則:
1. 二八法則:在任何一組東西中,最重要的只佔其中一小部分,約20%,其餘80%的儘管是多數,卻是次要的;在最佳化實踐中,我們將精力集中在最佳化那20%最耗時的代碼上,整體效能將有顯著的提升;這個很好理解。函數A雖然代碼量大,但在一次正常執行流程中,只調用了一次。而另一個函數B代碼量比A小很多,但被調用了1000次。顯然,我們更應關注B的最佳化。
2. 編完代碼,再最佳化;編碼的時候總是考慮最佳效能未必總是好的;在強調最佳效能的編碼方式的同時,可能就損失了代碼的可讀性和開發效率;
工具:
1 Gprof
工欲善其事,必先利其器。對於Linux平台下C++的最佳化,我們使用gprof工具。gprof是GNU profile工具,可以運行於linux、AIX、Sun等作業系統進行C、C++、Pascal、Fortran程式的效能分析,用於程式的效能最佳化以及程式瓶頸問題的尋找和解決。通過分析應用程式運行時產生的“flat profile”,可以得到每個函數的調用次數,消耗的CPU時間(只統計CPU時間,對IO瓶頸無能為力),也可以得到函數的“呼叫歷程圖”,包括函數調用的層次關係,每個函數調用花費了多少時間。
2. gprof使用步驟
1) 用gcc、g++、xlC編譯器時,使用-pg參數,如:g++ -pg -o test.exe test.cpp編譯器會自動在目標代碼中插入用於效能測試的代碼片斷,這些代碼在程式運行時採集並記錄函數的調用關係和調用次數,並記錄函數自身執行時間和被調用函數的執行時間。
2) 執行編譯後的可執行程式,如:./test.exe。該步驟運行程式的時間會稍慢於正常編譯的可執行程式的已耗用時間。程式運行結束後,會在程式所在路徑下產生一個預設檔案名稱為gmon.out的檔案,這個檔案就是記錄程式啟動並執行效能、調用關係、調用次數等資訊的資料檔案。
3) 使用gprof命令來分析記錄程式運行資訊的gmon.out檔案,如:gprof test.exe gmon.out則可以在顯示器上看到函數調用相關的統計、分析資訊。上述資訊也可以採用gprof test.exe gmon.out> gprofresult.txt重新導向到文字檔以便於後續分析。
以上只是gpro的使用步驟簡介,關於gprof使用執行個體詳見附錄1;
實踐
我們的程式遇到了效能瓶頸,在採用架構改造,改用記憶體資料庫之前,我們考慮從代碼級入手,先嘗試代碼級的最佳化;通過使用gprof分析,我們發現以下2個最為突出的問題:
1.初始化大對象耗時
分析報告:307 6.5% VOBJ1::VOBJ1@240038VOBJ1
在整個執行流程中被調用307次,其對象初始化耗時佔到6.5%。
這個對象很大,包含的屬性多,屬於基礎資料結構;
在程式進入建構函式函數體之前,類的父類對象和所有子成員變數對象已經被產生和構造。如果在建構函式體內位其執行賦值操作,顯示屬於浪費。如果在建構函式時已經知道如何為類的子成員變數初始化,那麼應該將這些初始化資訊通過建構函式的初始化列表賦予子成員變數,而不是在建構函式函數體中進行這些初始化。因為進入建構函式函數體之前,這些子成員變數已經初始化過一次了。
在C++程式中,建立/銷毀對象是影響效能的一個非常突出的操作。首先,如果是從全域堆中產生對象,則需要首先進行動態記憶體分配操作。眾所周知,動態分配/回收在C/C++程式中一直都是非常耗時的。因為牽涉到尋找匹配大小的記憶體塊,找到後可能還需要截斷處理,然後還需要修改維護全域堆記憶體使用量情況資訊的鏈表等。
解決方案:我們將大部分的初始化操作都移到初始化列表中,效能消耗降到1.8%。
2.Map使用不當
分析報告:89 6.8% Recordset::GetField
Recordset的getField被調用了89次,效能消耗佔到6.8%;
Recordset是我們在在資料庫層面的封裝,對應取出資料的記錄集;(用過ADO的朋友很熟悉);由於我們使用的是底層c++資料庫介面,通過對資料庫原始api進行一層封裝,從而屏蔽開發人員對底層api的直接操作。這樣的封裝,帶來的好處就是不用直接與底層資料庫互動,在代碼編寫方面方便不少,代碼可讀性也很好;帶來的問題就是效能的損失;
分析:(2點原因)
1)在GetField函數中,使用了map[“a”]來查詢資料,如果找不到“a”,則map會自動插入key ”a”,並設value為0;而m.find(“a”)不會自動插入上述pair,執行效率更高;原有邏輯:
string Recordset::GetField(const string &strName){ int nIndex; if (hasIndex==false) { nIndex = m_nPos; } else { nIndex = m_vSort[m_nPos].m_iorder; } if (m_fields[strName]==0) { LOG_ERR("Recordset::GetField:"<<strName<<" Not Find!!"); } return m_records[nIndex].GetValue(m_fields[strName] - 1) ;}
改造後的邏輯:
string Recordset::GetField(const string &strName){ unordered_map::iterator iter = m_fields.find(strName); if (iter == m_fields.end()) { LOG_ERR("[Recordset::GetField] "<< strName <second - 1) ;}
調整後的Recordset::GetField的執行時間約是之前的1/2;且易讀性更高;
2)在Recordset中,對於每個欄位的儲存,使用的是map m_fields; g++中的stl標準庫中預設使用的紅/黑樹狀結構作為map的底層資料結構;
通過附錄中的文檔2,我們發現其實有更快的結構, 在效率上,unorder map優於hash map, hash map 優於 紅/黑樹狀結構;如果不要求map有序,unordered_map 是更好的選擇;
解決方案:將map結構換成unordered_map,效能消耗降到1.4%;
總結
我們修改不到30行代碼,整體效能提升10%左右,效果明顯;打蛇打七寸,效能最佳化的關鍵在於找准待最佳化的點,之後的事,也就水到渠成;
附錄:
附1:prof工具介紹及實踐
附2: map hash_map unordered_map 效能測試
如果您認為這篇部落格讓您有些收穫,請點擊右下角的【推薦】按鈕。
Posted by: 大CC | 05JUN,2013
部落格:blog.me115.com
微博:新浪微博