1. 程式人生 > >如何編寫高效的程式碼(2014/6/1)

如何編寫高效的程式碼(2014/6/1)

編寫高效的程式碼有兩個條件:選擇好的演算法和資料結構,編寫編譯器能夠優化以轉換成高效可執行的程式碼。前者是基礎和前提,即使後者做的足夠好,但是選用了錯誤的演算法和資料結構,優化也不起作用,這個一點要搞清楚。本文的內容的側重於後者。

1 計算機系統架構


L1和L2位於CPU晶片上,L3被各個CPU共享。由於成本的考慮,L3,L2,L1的大小依次遞減。以INTEL XEON E7-8891V2為例,L3 37.5 MBL2 2.5 MBL1 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)減少函式呼叫