上篇確立演算法為效能關鍵,本文揭示編譯器優化如何重塑代碼執行面貌。高級語言代碼長短、變數多寡與機器碼效能關係微弱,盲目手動微優化往往適得其反。
遞歸求和單向鏈表,直觀寫法空間複雜度 O(n) (調用棧累積):
let rec sum ls =
match ls with
| [] -> 0
| x::xs -> x + sum xs
引入累加器參數實現尾遞歸:
let sum ls =
let rec sum' ls acc =
match ls with
| [] -> acc
| x::xs -> sum' xs (acc + x)
sum' ls 0
F# 編譯器自動將尾遞歸轉換為迴圈(IL 反編譯為 C# 可見):
public override int Invoke(FSharpList ls, int acc) {
while (true) {
var list = ls;
if (!(list is FSharpList._Cons)) return acc;
var cons = (FSharpList._Cons)list;
ls = cons.get_Tail();
acc += cons.get_Head();
}
}
同樣是遞歸寫法,編譯器優化後空間複雜度降為 O(1),效能媲美手寫迴圈。
這些優化由編譯器系統性、全域性完成,人工極難超越。
C/C++ 指標別名問題限制優化:
for (int i=0; i<20000; i++)
for (int j=0; j<20000; j++)
x[i][j] = y[i-2][j+1] * y[i+1][j-2];
編譯器無法確定 x 與 y 是否重疊,被迫保守生成代碼。Fortran `restrict` / C99 `restrict` / C# 陣列無別名,允許激進向量化。OCaml 編譯器甚至能識別下標關係、合併迴圈、提升不變量,超越 C/C++ 20%+。
最長公共子串問題,長度 2000 陣列:
| 語言/實現 | 耗時 |
|---|---|
| C (手動優化) | 0.8s |
| C++ | 2.3s |
| OCaml (解釋) | 0.6s |
| OCaml (編譯) | 0.3s |
| Java (舊 JIT) | 80s |
| Java (新 JIT) | 0.7s (+1s 啟動) |
JIT 編譯器利用運行時資訊(熱點、型別專精化)超越靜態編譯。
下篇將探討「演算法一致、編譯器不優化」前提下的硬體層面效能關鍵——快取與局部性。