效能最佳化Linux下程式效能分析工具gprof
gprof 安裝在Linux 系統的 /usr/bin 目錄下. 它能剖析你的程式,並分析出程式的哪一個部分在執行時最費時間.
gprof 將告訴你程式裡每個函數被調用的次數和每個函數執行時所佔時間的百分比. 你如 果想提高你的程式效能的話這些資訊非常有用.
為了在你的程式上使用 gprof, 你必須在編譯器時加上 -pg 選項. 這將使程式在每次 執行時產生一個叫 gmon.out 的檔案. gprof 用這個檔案產生剖析資訊.在你運行了你的程式併產生了 gmon.out 檔案後你能用下面的命令獲得剖析資訊:
gprof <program_name>
參數 program_name 是產生 gmon.out 檔案的程式的名字. 為了說明問題,在程式中增加了函數count_sum()以消耗CPU時間,程式如下
#include <stdio.h>
static void my_print (char *);
static void my_print2 (char *);
main ()
{
char my_string[] = "hello world!";
my_print (my_string);
my_print2 (my_string);
my_print (my_string);
}
void count_sum()
{
int i,sum=0;
for(i=0; i<1000000; i++)
sum += i;
}
void my_print (char *string)
{
count_sum();
printf ("The string is %s ", string);
}
void my_print2 (char *string)
{
char *string2;
int size, i,sum =0;
count_sum();
size = strlen (string);
string2 = (char *) malloc (size + 1);
for (i = 0; i < size; i++) string2[size -1 - i] = string[i];
string2[size] = '';
for(i=0; i<5000000; i++)
sum += i;
printf ("The string printed backward is %s ", string2);
}
$ gcc -pg -o hello hello.c
$ ./hello
$ gprof hello | more
將產生以下的輸出
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total time seconds seconds calls us/call us/call name
69.23 0.09 0.09 1 90000.00 103333.33 my_print2
30.77 0.13 0.04 3 13333.33 13333.33 count_sum
0.00 0.13 0.00 2 0.00 13333.33 my_print
由以上資料可以看出,執行my_print()函數本身沒花費什麼時間,但是它又調用了
count_sum()函數,所以累計秒數為0.13.
技巧: gprof 產生的剖析資料很大, 如果你想檢查這些資料的話最好把輸出重新導向到一個 檔案裡。
.
影響軟體效率的因素
軟體效能由兩大因素決定,如。軟體設計是語言無關的,設計者必須充分瞭解軟體的主題功能,設計不良造成的效能問題不要指望通過編寫代碼解決。編碼對於效能也是有影響的,比如你不應該將常量運算式放入迴圈中,這樣會增加計算次數。
軟體設計中對效能的影響又包含兩個因素,一個是演算法和資料結構,一個是程式分解。從技術觀點來看,一個程式就是一個演算法,不過通常術語“演算法和資料結構”指的是尋找、排序、訪問、壓縮以及操作大型的資料集合。演算法和資料結構對程式的效能同常有影響,但並不是唯一的因素。程式分解包含了將一個完整的大任務分解成一系列相互關聯的子任務、對象體繫結構、函數、資料和業務流。
編碼對效能的影響也可以分成四個子因素,如。
C++對C做了補充和完善,但是有些C++的構件對效能作出了一定的犧牲,這是語言的因素。
當前的不少作業系統設計會讓你感覺記憶體似乎不會用完,可以並存執行,CPU專門為自己的程式服務,統一的記憶體訪問模式,但是實際上即便是採用虛擬記憶體設計的作業系統,記憶體也會有用完的時候;CPU不可能總是執行你的程式,而是所有程式輪流使用CPU,每次獲得一定的時間片來執行;記憶體訪問模式不是統一的,訪問硬碟和記憶體以及快取都是不一樣的,因此效能也不一樣;在一個單CPU的機器上,並存執行往往不能帶來好處,可能還會導致程式運行速度變慢。
不同的庫提供了相同的函數,但是效能表現不一定相同,比如sprintf和itoa。
編譯器最佳化完全取決於編譯器實現廠商,因此差異很大。
編寫高效能代碼的建議
以效能為名,將設計或代碼變得更加複雜,從而導致可讀性更差,但是並沒有經過驗證的效能需求(比如實際的度量資料和與目標的比較結果)作為正當理由,因此本質上對程式並沒有好處。
首先,應該關注代碼儘可能的清晰易讀,清晰的代碼更容易最佳化。先讓程式做正確的事情,然後再讓它更快速應該較容易。
但是要掌握一些慣用技巧,比如優先使用首碼形式的++,--操作,傳遞引用,延遲定義變數,使用初始化列表初始化成員變數。
不要過早的使用內聯,應該通過工具分析哪些函數真正需要內聯。
當真正需要最佳化效能的時候,首先使用現代效能分析工具找出程式中的主要瓶頸。
但是作為程式庫的設計者,預測哪些操作最後會用於效能敏感的客戶代碼是幾乎不可能的。在這種特殊情況下,經驗、猜測和對客戶代碼進行大範圍的測試這些手段幾乎都要用到。比如:ACE就將自己的wrapper facade層的C++類的成員函數內聯,我相信這是經過考驗的決定。
構造和析構
如果有繼承的情況下,一個衍生類別建構函式總是先調用父類的建構函式,並且編譯器要產生代碼以正確的設定虛函數表以及虛函數指標(因為父類通常都需要虛解構函式,所以虛函數表通常是避免不了的),以及父類和子類成員變數的初始化。如果這個繼承體系較深的話,付出的代價更多。解構函式因為是建構函式的逆過程,同樣也要付出較多的代價。因此在一個效能要求很高的系統中,我們設計C++類需要考慮到這個因素,避免產生多餘的父類。(這點很難把握,因為一個符合is a的設計,父類會很自然的被設計出來)
如果在組合情況下,一個類的建構函式必須正確初始化其成員變數,如果成員變數有建構函式,那麼也有可能會被調用,解構函式同樣如此。
虛函數
虛函數好處是利用動態類型提供了更好的抽象,客戶代碼只需要和基類介面打交道,代碼更優雅和便於維護。但是虛函數可能會在三個方面影響效能:
1)虛函數表的布局(前面已有論述)的初始化會對構造和解構函式造成效能開銷
2)虛函數都是通過運行時查表,然後調用合適的函數,因此比直接的函數調用慢
3)編譯器通常對虛函數的內聯處理較困難複雜,有些編譯器不支援內聯虛函數
前面兩個不會造成效能的負擔,因為如果不使用虛函數,作為一個常用的替代方案,可以給基類定一個一個類型變數,然後每一個子類型在建構函式中設定該變數的值。這些開銷和初始化虛函數表指標變數是相等的。然後,還要使用switch/case語句判斷類型,並且進行強制類型轉換(從基類指標到子類指標),這些開銷等價於虛函數表的尋找操作。
看來唯一的問題就是如果虛函數比較簡單而且被頻繁調用,那麼不能內聯將導致效能的損失。
虛函數帶來的效能問題有時候是可以消除的,想想虛函數和繼承的密切關係,如果沒有繼承的話,也就不會出現虛函數。有一種方法可以讓我們減少對繼承的依賴,模板。
舉個例子,如果有一個MyString類,要支援安全執行緒,我們可能會有mutex和critical section兩種線程同步方案,假設存在兩個類,它們都實現了lock和unlock函數:
class CriticalSection
{
public:
void lock();
void unlock();
...
};
class Mutex
{
public:
void lock();
void unlock();
...
};
在這裡,我們並不打算讓這兩個類派生自LockBase類,使得lock和unlock函數成為虛函數,它們就是普通的成員函數。我們將MyString設計成模板類:
template<typename lock>
class MyString
{
private:
lock _lock;
};
使用模板技術,我們將運行時多態替換成編譯時間多態,我們有效消除了虛函數,因此lock和unlock函數可以被內聯的機率大大增加。