編寫高效的代碼有兩個條件:選擇好的算法和數據結構,編寫編譯器能夠優化以轉換成高效可執行的代碼。前者是基礎和前提,即使後者做的足夠好,但是選用了錯誤的算法和數據結構,優化也不起作用,這個一點要搞清楚。本文的內容的側重於後者。
1 計算機系統架構
L1和L2位於CPU芯片上,L3被各個CPU共享。由於成本的考慮,L3,L2,L1的大小依次遞減。以INTEL XEON E7-8891V2爲例,L3 :37.5 MB,L2 :2.5 MB,L1 :640 KB。
2 各種存儲的速度比
|
|
寄存器 |
|
L1 | 10-13 |
L2 | 10-12 |
L3 | 10-11 |
內存 | 10-10 |
磁盤 | 10-3 |
從上表中可以看出,內存比磁盤快1000萬倍。
3 進程的內存分佈
共享映射庫的起始地址是0x40000000(1G),BSS: 未初始化的全局變量。
4 函數調用的開銷
ebp寄存器指向當前幀的棧底,esp寄存器指向當前幀的棧頂。函數被調用時,都會生成一個新的幀。返回地址:函數調用返回調用者後,待執行的代碼的地址。在調用函數之前,調用者在自己的棧內構造參數。緩衝區溢出攻擊的原理:覆蓋被保存的ebp和返回地址,執行攻擊者指定的函數。
5 代碼優化
編寫高效的代碼有兩個步驟:選擇好的算法和數據結構,編寫編譯器能夠優化以轉換成高效可執行的代碼。第一個步驟是前提。即使第二部做的足夠好,但是選用了錯誤的算法和數據結構,優化也不起作用。本小節後續的優化措施指的是後者。
5.1 函數調用和系統調用
(1) 從第四小節可以看出,函數調用的開銷不菲。所以在調用次數非常高的函數內,儘量少的使用函數,特別是小函數。
(2) 儘量把循環內部的函數調用移到循環外部。
帶來的副作用損害了代碼的可讀性,可維護性。
系統調用的的開銷:
1,段的切換。
2,當前數據、指令預取隊列的刷新和重建。
3,數據複製。5.2 利用內存局部性原理
時間局部性:如果數據被訪問,則不久之後該數據可能被再次訪問。
空間局部性:如果某個存儲單元被訪問,則不久之後附近的存儲單元也會被訪問。
例:沒有利用空間局部性的代碼
for(i=0;i<M;i++)
for(j=0;j<N;j++)
sum += a[j][i];
例:利用空間局部性的代碼
for(i=0;i<M;i++)
for(j=0;j<N;j++)
sum += a[i][j];
重複利用同一變量有良好的時間局部性。
5.3 適應CPU的指令流水線
執行指令的基本流程:
1 從內存讀取指令到L1
2 對L1中的指令譯碼
3 執行單元執行指令
現代的CPU進行分支預測,在指令高速緩存區形成一個指令流水線,如果預測失敗,需要重新從內存讀取指令,譯碼形成一個新的指令流水線,對CPU而言,操作內存的開銷非常大(2個數量級的差別),所以代價非常高。
優化措施:
(1) 儘量減少分支
儘量不用if/else switch。用switch代替if/else,因爲跳轉指令更少。
經過測試if 和?:的彙編代碼是一樣的,所以性能也一樣。
(2 )減少小循環,因爲循環裏面有跳轉指令
(3)減少函數調用