優化是程式設計師永恆的追求——渴望程式碼又小又快。現代編譯器已能產生高度優化的機器碼,但編譯器僅做「等價轉換」,而非「等效轉換」。理解編譯器優化的邊界,才能在關鍵路徑上以手工組合語言突破效能極限。
// 原始碼
for(i=0; i<10; i++) {
j = i;
k = j + i;
}
// 編譯器可能轉換為
for(i=0; i<10; i++); // 空迴圈
j = i; k = j + i; // 碼外提
// 甚至進一步常數摺疊
j = 10; k = 20;
原理:若中間計算結果在被使用前已被覆寫,則該計算可省略。但編譯器不會自動將高斯求和改為 return 5050;,因為這屬於語意層面的「等效」而非語法層面的「等價」。
const unsigned long nGiga = 1024L * 1024L * 1024L;
// 編譯期直接變為
const unsigned long nGiga = 1073741824L;
// 原始
a[0] = b*c;
a[1] = b+c;
a[2] = d*e;
a[3] = b*d + c*d;
a[4] = b*d*e + c*d*e;
// 優化後
a[0] = b*c;
a[1] = b+c;
a[2] = d*e;
a[3] = a[1] * d; // 重用 b+c
a[4] = a[3] * e; // 重用 (b+c)*d
// 指令排序進一步優化(利用暫存器熱度)
a[0] = b*c;
a[1] = b+c;
a[3] = a[1] * d;
a[4] = a[3] * e;
a[2] = d*e;
現代超純量處理器可並行執行無相依指令,合理排序可提升 IPC (Instructions Per Cycle)。
編譯器透過圖著色演算法進行全域暫存器分配,目標是將最高頻使用的變數對應到實體暫存器,減少記憶體存取。
for(i=0; i<1000; i++)
for(j=0; j<1000; j++)
for(k=0; k<1000; k++)
for(l=0; l<1000; l++)
do_something(i, j, k, l);
最佳分配順序:l > k > j > i(內層迴圈變數存取頻率最高)。但實際受限於暫存器數量(x86 僅 8 個通用暫存器),編譯器需權衡溢出成本。
int a=0;
for(int i=1; i<10; i++)
for(int j=1; j<100; j++)
a += (i*j);
編譯器產生的關鍵優化:
t = i*j,內層迴圈改為 t += i(加法取代乘法)dec/jnz 比 inc/cmp/jl 更快)eax=a, edx=i, ecx=t, esi=jxor eax, eax ; a=0
mov edx, 1 ; i=1
push esi ; 保存 esi
nexti:
mov ecx, edx ; t=i
mov esi, 999 ; j=999 (1000次)
nextj:
add eax, ecx ; a+=t
add ecx, edx ; t+=i
dec esi
jne nextj
inc edx ; i++
cmp edx, 10
jl nexti
pop esi
指令執行四階段:取指 → 取資料 → 運算 → 存結果。若 4 條指令無相依,可在 7 個週期內完成(原本需 16 週期)。
Intel Hyper-Threading 在單一實體核心模擬兩個邏輯核心,共享執行單元但擁有獨立架構狀態,提升資源利用率。但需 OS 排程器支援(Win2K/Linux 2.4+)。
int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};
int avg=max=min=a[0];
for(i=1; i<20; i++) {
avg+=a[i];
if(max<a[i]) max=a[i];
else if(min>a[i]) min=a[i];
}
avg/=i;
VC++ 產生大量 mov 指令逐一寫入陣列,未使用 rep movsd 區塊複製。手工優化可預先計算常數陣列或使用字串指令。
jmp SHORT 占位指令編譯器插入無條件跳轉作為對齊填充,使後續指令落在 16 位元組邊界,利於取指解碼並行。刪除可能反而降低效能。驗證方法:大樣本計時測試(百萬次迴圈、關閉服務、斷網、提高程序優先級、必要時在 FreeDOS 下測試)。
/O2 /Ot /GL 等)記住:過早優化是萬惡之源。先寫正確、清晰的高階語言碼,再以數據驅動優化決策。