Visual C++ 6以debug模式編譯很拙笨,為何要做無用功?
先看圖片
請注意矩形內的代碼,它們的作用是——把eax移動到【ebp-18h】這個位置,然後從【ebp-18h】取出數據賦值給eax。。。可是,這樣做並沒有卵用啊,緊跟著的那句代碼還不是把eax賦值給【ebp-14h】了,為什麼剛開始不直接這樣做呢??下面這個圖片是回復Milo Yip大俠的回答:
其實只需要比較入門的編譯原理知識就可以清晰的讀懂題主貼出的代碼。
題主貼出來的代碼展現了Visual C++ 6採用了下述代碼生成策略。
首先,所有源碼中的運算,包括load/store,都會在IR層面生成出臨時變數。
這是編譯器里很常見的中間代碼生成策略,好處是讓前端邏輯非常簡單且一致,而由其產生的冗餘可以輕易在後續優化中消除。所以題主原本貼的代碼:int *o = new int[10];
delete []o;
在編譯器內的IR層面生成出了這樣的IR:
// int *o = new int[10];
tmp0 = new int[10]; // operation: produce new pointer
int *o = tmp0; // operation: store to local variable
// delete []o;
tmp1 = o; // operation: load from local variable
delete []tmp1; // operation: array delete
然後,由於Visual C++在debug模式幾乎禁用了所有優化,所以本來用於消除IR中的冗餘的優化都沒做。
而用release模式編譯的話,Visual C++可以採用copy propagation(複寫傳播)來消除掉這段代碼里的臨時變數。接下來,Visual C++在debug模式下也不做任何實質上的寄存器分配;所有用戶聲明的變數和編譯器內部生成的臨時變數都在棧幀?上分配了slot,都在內存里。
而用release模式編譯的話,變數會儘可能被分配到寄存器里,大幅減少這種內存-寄存器-內存之間的拷貝。最後,生成最終的機器碼時,數據就在這些內存里的棧幀的slot之間移動。由於x86的運算和數據移動指令都不接受兩個操作數都在內存里,所以必須藉助臨時寄存器來完成實際運算。
所以最終生成的代碼就是這樣:// stack frame layout:
// esp: stack pointer
// ebp: frame pointer
//
// [ebp - 1ch]: tmp1
// [ebp - 18h]: tmp0
// [ebp - 14h]: o (local slot)
// [ebp - 10h]: local slot
// [ebp - ch] : local slot
// [ebp - 8h] : local slot
// [ebp - 4h] : local slot
// [ebp + 0h] : saved caller frame pointer
// [ebp + 4h] : return address
// [ebp + 8h] : arg0
// [ebp + ch] : arg1
// [ebp + ...]: arg ...
// int *o = new int[10];
push 28h // arg 0 to operator new; 0x28 == 40 == sizeof(int)*10 == sizeof(int[10])
call operator new[] // return value in eax
add esp, 4 // balance the stack (cdecl)
mov [ebp - 18h], eax // tmp0 = new int[10]
mov eax, [ebp - 18h]
mov [ebp - 14h], eax // o = tmp0
// delete []o
mov ecx, [ebp - 14h]
mov [ebp - 1ch], ecx // tmp1 = o
mov edx, [ebp - 1ch]
push edx // arg 0 to operator delete[]
call operator delete[]
add esp, 4 // balance the stack (cdecl)
(棧幀布局的一些細節取決於編譯參數,例如是否使用了SEH)
就這樣嗯。因為這是按照操作一個一個生成的,你圈的這兩行剛好在兩個操作裡面。這是最簡單的生成機器碼的方法。現在的編譯器如果你把優化全部關掉了也會生成這樣的代碼。
這樣編譯是嚴格按照語言的語義生成代碼,主要是為了方便調試,你在調試的時候能看到的所有變數都要存到內存中的,每個變數一個單元。運行時基本沒法得知當前時刻寄存器對應哪個變數。優化過的代碼自然會省去這些無用功,但是調試起來就會出現監視不了變數和跳行的情況。
這確實是因為沒有開優化,但樓上都沒有說為什麼不優化就會生成這種傻缺代碼,我來解釋一下。
最簡單的代碼生成對每個語句/表達式部分都有一個相應的模板,這些模板除了要給當前語句/表達式生成代碼以外還要生成一些額外的代碼(就是固定的模板代碼)來和上下文中的其他模板配合工作,以便於遞歸下降式地生成代碼。比如,最簡單的模板就是規定每個表達式的求值結果都要放進一個固定的寄存器(比如eax),這樣所有模版代碼就默認從eax中取得上一個表達式的求值結果。這個你可以參考一下Stanford 的Compiler 課程中提到的1-register stack machine。
然而這種代碼生成方式有一個很大的弊端,就是當表達式非常簡單時會生成冗餘代碼。比如a+b 這個表達式生成的代碼應該是直接取變數a的值到寄存器(比如eax),再給eax 加上b的值。但是這種naive 的代碼生成策略依然會按部就班地把讀取a 和b 的值的代碼都插入上面所說的模板中(move eax a;move eax b),因為eax 之前被用於存貯a 的值,所以在取b 的值之前要先把eax 保存到一個臨時變數中(實際中為提前計算好的一塊在當前activition record 中的內存,比如move [ebp -XXX], eax;XXX為提前計算好的常數)然後把b 的值和這個臨時變數的值相加再放回eax中供下一個表達式使用。這樣就產生了冗餘。
老一些的編譯器在生成了這樣的代碼之後就會開始實施窺孔優化,什麼constant propagation 、copy propagation 、strength reduction 、 instruction schedule等等優化策略一個個施加上去,最後才能得到接近(超越)人手寫彙編效率的代碼。猜測第一個mov和後兩個mov分別屬於不同的語法樹節點 任何編譯器在O0都是笨拙的,因為就是這麼設計的
我了個燙!這是 VC6 啊,現在能看見這古董也不容易,摺疊我把
1. 根據編譯器的代碼生成策略。不開優化的話,每個變數在寫到寄存器發生修改之後就要寫回內存。
2. 開調試的話,你不寫回內存,調試器就看不到寄存器里的值了。
3. 開優化的話,這種代碼自然會被編譯器優化掉。推薦閱讀:
※如何拒絕編譯器將函數內聯處理?
※為什麼編譯器不能「合成「純虛析構函數的函數定義??
※第一個 C 語言編譯器是用什麼語言編寫的?
※編程語言是語法比較重要還是編譯器的具體實現比較重要?
※符號表和抽象語法樹是什麼關係?兩者在編譯器設計中是否必需?