gprof的效能優化實踐

工程師WWW發表於2016-04-26

優化準則:

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,執行效率更高;原有邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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) ;
}

改造後的邏輯:

1
2
3
4
5
6
7
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 效能測試

相關文章