在硬件資源昂貴的時代,編程人員非常注重程序的性能,以期望用盡可能少的硬件資源完成盡可能多的事情。隨著科技的發展,
摩爾定律的魔力使得硬件資源已越來越便宜,速度也越來越快,似乎性能已不是編程人員所需關注的事情了。然而在一個競爭
與發展的時代,軟件的功能越來越復雜,用戶的操作體驗越來越重要,而且競爭越來越激烈,誰能以更優勢的價格,更好的操
作體驗,完成更多更復雜的事情,誰就將在激烈競爭中勝出。因而軟件的性能優化必將一直是軟件領域所要關注的內容之一。
雖然軟件的性能優化貫穿了設計與編碼的整個過程,本文也將從設計與編碼兩個層次對性能優化進行分析。本文還將從CPU、內
存、磁盤、網絡四個方面描述性能問題分析的過程。
2.設計出來的性能
1)系統架構
控制流與數據流?減少不必要的模塊
2)程序結構
多線程程序
鎖的粒度、各種鎖/信號量的性能對比
共享內存通信
降低靈活性以獲取高性能。
減少不必要的重復判斷(SHTTP/HTTP)
3)接口設計
好的接口給予使用者充分的靈活性
4)數據結構與算法
Linux內存管理,數量小時使用鏈表
3.編碼的藝術
1)內存訪問與文件
減少new/delete或malloc/free操作減少換頁
減少文件打開與關閉操作
減少文件讀寫次數(減少系統調用)
2)減少不必要的運算
消除重復運算
循環中的運算
最忙的循環放在裡面
3)語言及庫函數特性的利用
if與case語句
構造與析構
宏與內聯函數
遲緩型計算
減少臨時變量
緩存字符串的長度
不必要的memset
4)硬件特性的利用
字節對齊
移位與乘除2
性能熱點用匯編實現
4.性能分析工具-callgrind
valgrind系列工具因為免費,所以在linux系統上面最常見。callgrind是valgrind工具裡面的一員,它的主要功能是模擬cpu的cache,能夠計算多
級cache的有效、失效次數,以及每個函數的調用耗時統計。
callgrind的實現機理(基於外部中斷)決定了它有不少缺點。比如,會導致程序嚴重變慢、不支持高度優化的程序、耗時統計的結果誤差較大等,
更多的外部工具有oprofile,gprof,tprof,Rational Quantify and Intel VTune
5.編譯器參數的優化
大家要記住的是,編譯器絕對比想象的要強大的多。編寫編譯器的人大都是十年、幾十年代碼編寫經驗的科學家!你能簡單想到的,他們都已經想
到過了。普通的編譯器,可以支持大部分已知的優化策略以及多媒體指令。至於哪個編譯器更好,大部分人的觀點是:intel。Intel畢竟是最優秀的
cpu提供者,他們的編譯器考慮了很多cpu的特性,跑的更快。但目前intel編譯器有一些比較弱智的地方,即它只識別自己的cpu,不是自己的cpu,
就認為是最差的i386-i686機器,從而不能在amd等平台上面支持sse功能。我們在linux上面寫代碼,一般更加喜歡流行的編譯器,比如gcc。
Gcc的優點是它更新快,開源,bug修改迅速。正因為他更新快,所以它能夠支持部分C03的規范。
5.1 gcc支持的優化技術
1) 函數內聯
函數調用的過程是:壓入參數到堆棧、保護現場、調用函數代碼、恢復現場。當一個函數被大量調用的時候,函數調用的開銷特別巨大。函數內
聯是指把這些開銷都去除,而直接調用代碼。函數內聯的不好之處是難以調試,因為函數實際上已經不存在了。
2) 常量預先計算
a = b + 1000 * 8
對於這段代碼,程序會預先計算b + 1000 * 8,從而變成:
a = b+ 8000
3) 相同子串提取
a=(b+1)*(b+1)
這裡,b+1需要計算2次,可以只用計算一次:
tmp=b+1
a=tmp*tmp
4) 生存周期分析
這是一個比較高級的技術。假設有代碼:
a=b+1
c=a+1
在執行的時候,因為第二句依賴第一句,所以2句是線性執行。
但編譯器其實可以知道,c就是等於b+2,所以代碼變成:
a=b+1
c=b+2
這樣,這2句就沒有任何關系來了,執行的時候,cpu可以並行執行它們。
5) 清除跳轉
看如下代碼:
int func()
{
int ret = 0;
if(xxx)
ret=5;
else if(yyy)
ret=6;
return ret;
}
當條件xxx滿足的時候,程序還會跳到下面執行,但其實是沒有必要的。編譯器會把它變成:
int func()
{
if(xxx)
return 5;
else if(yyy)
return 6;
}
6) 循環展開
循環由幾個部分組成:計數器賦值、計算器比較、跳轉。每次循環,後面2步都是必須的消耗。把循環內的代碼拷貝多份,可以大大減少
循環的次數,節約後面2步的耗時。參考:
for(int counter = 0; counter < 4; count++)
xxx;
可以變成:
xxx;
xxx;
xxx;
xxx;
編譯器不僅僅可以展開普通循環,它還能展開遞歸函數。原理是一樣的,遞歸其實是一個不定長的借用了堆棧的循環。
7) 循環內常量移除
for(int idx=0;idx<100;idx++)
a[idx]=a[idx]*b*b;
因為b*b在循環體內的值固定(常量),所以代碼可以變成:
tmp=b*b;
for(int idx=0;idx<100;idx++)
a[idx]=a[idx]*tmp;
8) 並行計算
大家都知道,現代cpu支持超流水線技術,同時可以執行多條語句。多條語句能否同時執行的限制是不能互相依賴。編譯器會自動幫我們把
看起來單線程執行的代碼,變成並行計算,參考:
d=a+b;
e=a+d+f;
可以變成:
tmp=a+f;
d=a+b;
e=d+tmp;
9) 表達式簡化
當年筆者在學習《離散數學》和《數字電路》的時候,總被眼花缭亂的布爾運算簡化題目難倒。gcc終於讓我松了一口氣。參考:
!a && !b
這句需要3步執行,但變成:
!(a || b)
只需要2步執行。
5.2 gcc重要優化選項
1) 內聯
-finline-small-functions
內聯比較小的函數。-O2選項可以打開。
-findirect-inlining
間接內聯,可以內聯多層次的函數調用。-O2選項可以打開。
-finline-functions
內聯所有可以內聯的函數。-O3選項可以打開。
-finline-limit=N
可以進行內聯的函數的最小代碼長度。注意,這裡是偽代碼,不是真實代碼長度。偽代碼是編譯器經過處理後的代碼。帶inline等標志的函數,默認
300行代碼即可內聯,不帶的默認50行代碼。和這個相關的選項是max-inline-insns-single和max-inline-insns-auto。
max-inline-insns-recursive-auto
內聯遞歸函數時,函數的最大代碼長度。
large-function-insns、large-function-growth、large-unit-insns等
函數內聯的副作用是它導致代碼變多,程序變長。這裡的幾個參數可以控制代碼的總長度,避免編譯後出現巨大的程序,影響性能和浪費資源。
2) -fomit-frame-pointer
不采用標准的ebp來記錄堆棧指針,節省了一個寄存器,並且代碼會更短。但據說在某些機器上面會導致debug模式出錯。實際測試表明,在gcc4.2.4以
下,O2和O3都無法打開這個選項。
3) -fwhole-program
把代碼當做一個最終交付的程序來編譯,即明確指定了不是編譯庫。這個時候,編譯器可以使用更多的static變量,來加快程序速度。
4) mmx/ssex/avx
多媒體指令,主要支持向量計算。一般來說,-march=i686、-mmx、-msse、-msse2是目前機器都支持的指令。
除了基本的多媒體支持外,gcc編譯器還支持-ftree-vectorize,這個選項告訴編譯器自動進行向量化,也是-O3支持的選項。
多說幾句。在平常的使用中,多媒體指令不是很常見(除非游戲)。如果你有幾個位表(bitset),它們需要進行各種位操作的話,多媒體指令還是挺有效果滴。
5.3 gcc大殺器-profile driven optimize
這是比較晚才出現的技術。其基本原理是:根據實際運行情況,縮短hot路徑的長度。編譯器通過加入各種計數器來監控程序的運行,然後根據計算出來
的實際訪問路徑情況,來分析hot路徑,並且縮短其長度。根據gcc開發者的說法,這種技術可以提高20-30%的運行效率。
其使用方式為:
編譯代碼,加上-fprofile-generate選項
到正式環境一段時間
當程序退出後,會產生一個分析文件
利用這個分析文件,加上-fprofile-use,重新編譯一次程序
舉個例子來說:
a=b*5;
如果編譯發現b經常等於10,那麼它可以把代碼變成:
a=50;
if(b != 10)
a=b*5;
從而在大多數情況下,避免了乘法消耗。
5.4 gcc支持的優化屬性(__attribute__)
aligned
可以設置對齊到64字節,和cpu的cache line看齊
fastcall
如果函數調用的前面2個參數是整數類型的話,這個選項可以用寄存器來傳遞參數,而不是用常規的堆棧
pure
函數是純粹的函數,任何時刻,同樣的輸入,都會有同樣的輸出。可以很方便依據概率來優化它。
5.5 gcc其他優化技術
#pragma pack()
對齊到一個字節,節省內存
__builtin_expect
直接告訴編譯器表達式最可能的結果,方便優化
編譯帶debug信息的小文件
以下代碼能夠大大減少編譯後程序大小,同時保留debug信息。其原理是外鏈一個帶debug的版本。
g++ tst.cpp -g -O2 -pipe
copy a.out a.gdb
strip --strip-debug a.out
objcopy --add-gnu-debuglink=a.gdb a.out
6.算法是核心
算法是程序的核心,一個程序的好壞,大部分是看起算法的好壞。對於一般的程序員來講,我們無法改變系統和庫的調用,只能根據
規則來使用它們,我們可以改變的是我們自己核心代碼的算法。
算法能夠十倍甚至百倍的提高程序性能。如快排和冒泡排序,在上千萬規模的時候,後者比前者慢幾千倍。
通常情況下,有如下一些領域的算法:
A)常見數據結構和算法
B)輸入輸出
C)內存操作
D)字符串操作
E)加密解密、壓縮解壓
F)數學函數
總上所述:性能問題通常體現在四個方面:CPU、內存、磁盤、網絡幾個方面。解決方法可以是修改代碼甚至程序結構以更充分的利用現有資源,
也可以是增加相應的硬件以增加資源供給。
作者將C高效編程的心得濃縮於20個技巧,並將這些技巧通過實驗的方式進行講解,簡明易懂,使人印象深刻。《》書中帶有大量的代碼實例,使讀者不僅能夠從理論上得以提高,而且還能夠輕松地在實踐中應用。·算法導論(超過50萬人閱讀的算法聖經!) ·謝謝你離開我(張小娴最新散文)內容簡介 《》從CPU與編譯器的運行機制講起,帶領讀者一步步了解程序的執行成本、編譯器的優化選項等,總結出許多C程序性能優化的技巧,並以實驗的方式進行了講解,簡明易懂,使人印象深刻。書中帶有大量的代碼實例,使讀者不僅能夠了解代碼優化的原理,還能夠輕松地在實踐中應用。 《》適合有一定基礎的C語言編程人員閱讀。作者精通高效編程,其開發的C編譯器,不僅適用於16位及32位系統,還能在GPU中對視頻數據進行實時編譯。作者將C高效編程的心得濃縮於20個技巧,並將這些技巧通過實驗的方式進行講解,簡明易懂,使人印象深刻。《》書中帶有大量的代碼實例,使讀者不僅能夠從理論上得以提高,而且還能夠輕松地在實踐中應用。·算法導論(超過50萬人閱讀的算法聖經!) ·謝謝你離開我(張小娴最新散文)內容簡介 《》從CPU與編譯器的運行機制講起,帶領讀者一步步了解程序的執行成本、編譯器的優化選項等,總結出許多C程序性能優化的技巧,並以實驗的方式進行了講解,簡明易懂,使人印象深刻。書中帶有大量的代碼實例,使讀者不僅能夠了解代碼優化的原理,還能夠輕松地在實踐中應用。 《》適合有一定基礎的C語言編程人員閱讀。作者精通高效編程,其開發的C編譯器,不僅適用於16位及32位系統,還能在GPU中對視頻數據進行實時編譯。作者將C高效編程的心得濃縮於20個技巧,並將這些技巧通過實驗的方式進行講解,簡明易懂,使人印象深刻。《》書中帶有大量的代碼實例,使讀者不僅能夠從理論上得以提高,而且還能夠輕松地在實踐中應用。·算法導論(超過50萬人閱讀的算法聖經!) ·謝謝你離開我(張小娴最新散文)內容簡介 《》從CPU與編譯器的運行機制講起,帶領讀者一步步了解程序的執行成本、編譯器的優化選項等,總結出許多C程序性能優化的技巧,並以實驗的方式進行了講解,簡明易懂,使人印象深刻。書中帶有大量的代碼實例,使讀者不僅能夠了解代碼優化的原理,還能夠輕松地在實踐中應用。 《》適合有一定基礎的C語言編程人員閱讀。
System.out.println((a == b) ? "YES" : "NO");
直接判斷輸出,避免了字符串定義,已經在內存中分配內存地址的過程!