[轉]C++的性能優化實踐

C++的性能優化實踐

內容目錄:

優化準則:

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 性能測試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章