1. 程式人生 > >破解/優化C++程式碼:常量合併

破解/優化C++程式碼:常量合併

這篇文章講的是常量合併,這是VC++編譯器最簡單的優化之一。  這種優化,是指編譯器在編譯時(編譯期間)直接計算出表示式的結果,在生成的程式碼中直接用計算結果替換表示式。 這樣就避免了程式在執行時執行這些計算花費的成本。

下面是一個例子 APP.cpp檔案中的 main 函式:

int main() { return 7 + 8; }

首先,關於這篇文章的一些須知:

我們將從命令列來構建程式(而不是Visual Studio)

我們會使用Visual Studio 2012。 特別注意的是,這個版本的編譯器會產生x64位程式碼(而不是已經過時的x86架構)在64位機子上編譯。

如果你想要繼續,請看下說明。實際上,你只需要從Visual Studio 列表裡選擇一個正確的變體。

(注意:如果你正在使用Visual Studio Express上的免費編譯器,它僅僅只能執行在x86上,但是也會順利生成x64的程式碼。對這個實驗同樣有用。)

我們可以通過命令 CL /FA App.cpp來構建示例程式。用/FA開關建立一個輸出檔案,用來儲存編譯器生成的彙編程式碼,可以輸入type App.asm來顯示:

PUBLIC  main

_TEXT   SEGMENT

main    PROC

        mov     eax, 15

        ret     0

main    ENDP

_TEXT   ENDS

END

有趣的是這條指令 mov ax,15—-僅僅將15賦值給暫存器EAX(根據x64呼叫標準的定義,x64函式將會設定一個int

值,作為函式的結果,並返回給呼叫者)。編譯器執行期間並沒有發出 7加8的指令。就像下面這樣:

PUBLIC  main

_TEXT   SEGMENT

main    PROC

        mov     eax, 7

        add     eax, 8

        ret     0

main    ENDP

_TEXT   ENDS

END

(注意看了,這兩段程式碼的最後一條指令,ret 0,是指將控制權返回給呼叫者,並從棧裡彈出0個位元組。不要被誤導認為是返回數值0給呼叫者!)

 

我猜到,你可能在想:這很好啊,但是哪個白痴會想到在程式碼裡寫 7+8 這樣的運算?的確,你是對的,但是編譯器會把這樣的結構看成是有副作用的巨集。看了下面的例子,你就會明白常量合併是一個很有用的優化方法:

#define SECS_PER_MINUTE  60

#define MINUTES_PER_HOUR 60

#define HOURS_PER_DAY    24

 

enum Event { Started, Stopped, LostData, ParityError };

 

struct {

    int        clock_time;

    enum Event ev;

    char*      reason;

}   Record;

 

int main() {

    const int table_size = SECS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY * sizeof Record;

    // rest of program

}

我們要建立一個足夠大的表儲存每一秒的記錄,所以table_size就是表的大小,用位元組表示。很容易檢視變數table_size的彙編指令:

1movDWORD PTR table_size$[rsp], 1382400; 00151800H

這兒沒有乘法指令,60*60*24*16=1382400 是在編譯時計算的。

事實上,我們窺探下編譯器的內部,會發現這種常量合併的運算非常簡單,它是由前端來執行的。它並不需要後端優化器笨重的提升能力。所以它總是存在的。不管你是開啟優化(使用 /O2)或者關閉優化(/Od)都沒什麼區別—–該優化總是自動執行的。

不管表示式有多複雜,我們都能在編譯期間進行常量合併嗎?—事實上,前端可以處理任意的常量算術表示式(甚至包括上面提到的sizeof,只要它們在編譯時能被計算出來)和運算子(+ – * / % << >> ++ 和 —)。你甚至可以使用布林值,邏輯運算子 和條件運算子if AND ?:。

有沒有常量合併需要後端優化器的時候呢?當然有,看下面的例子:

int bump(int n) { return n + 1; }

 

int main() { return 3 + bump(6); }

輸入命令cl /FA /Od App.cpp,會得到資訊:不能優化,謝謝!,輸入 App.asm,我們會得到:

mov ecx, 6

call [email protected]@[email protected] ; bump

add eax, 3

正如我們所預料的: ECX會儲存第一個引數6,根據x64呼叫約定,然後呼叫bump函式,結果返回給EAX,然後EAX再加3。

我們來看看如果我們使用cl /FA /O2 App.cpp 來進行優化,會發生什麼。

mov eax,10

後端優化器已經識別到bump函式很小,可以包含到呼叫者裡(我們在後面的章節將會講到這種優化方法,叫做行內函數)。它在編譯時就能夠估算出整個表示式的值,最後只剩下一條單指令。很神奇,對吧?