(譯者:這篇文章作者是一位美國的MVP,這是他的系列文章"Under the cover"的第一篇,文章的本意從最底層的角度來最佳化代碼的效能,並作為閱讀作者其他文章的技術基礎,這種通過這樣的做法雖然初看起來有些過分,但是對讀者瞭解.Net許多底層運作是十分有益的)
我們從使用visual studio進行Unmanaged 程式碼調試的基礎開始,以便大家可以更容易的學習今後的例子,並讓這篇文章作為我以後文章的基礎,雖然我也使用windbg,但visual studio已經成為了一個功能強大的調試工具,對於簡單的代碼最佳化問題反而更容易使用
當我們需要校調對效能要求很高的代碼時,查看IL通常不是最好的做法,因為JIT最佳化器會默默的最佳化我們的代碼,使用reflector或者ildasm你能很快發現release和debug模式下產生的IL代碼幾乎完全相同,那麼是什麼讓release模式的代碼運行起來如此迅速呢?這就是JIT最佳化的結果,通過查看managed代碼(IL代碼),我們沒有辦法看到這些最佳化,所以我們將通過native code(本地代碼)來尋找蛛絲馬跡。
必須說明我不提倡大家經常這樣做,我不贊成過早的進行最佳化,你必須使你的代碼先工作起來,你必須清楚的知道哪些代碼是不值得最佳化的,當你的程式碼完成後再來找那些需要提速的地方,當你發現有的地方10% 的代碼卻使用了70%的時間的時候,再回過頭去最佳化那10%的代碼.同時你總是應該把判斷的依據建立在對速度的實際測量上,而非僅僅是閱讀代碼,最後,其實資料結構的選擇比底層的最佳化重要的多
當然話又說回來,瞭解隱藏在.Net底層的秘密是非常有趣的,那就讓我們開始設定visualstudio,並動手實驗一個簡單的例子
首先我們需要一些實驗代碼
static void Main(string[] args) {
for (int i = 0; i < 10; i++) {
Console.WriteLine("Hello World!");
}
}
為了開啟Unmanaged 程式碼調試,我們需要對visual studio進行設定.開啟項目的屬性並進入Debug Tab,選擇該頁上的“Enable unmanaged code debugging”複選框
(注意,這個選項只對當前使用的配置有效,因此我們應該為我們使用的所有配置設定這個選項.)在迴圈的開始處插入一個斷點,並運行程式,你將會像往常一樣擊中一個斷點。這時你的螢幕應該看起來二(譯者:原文缺圖)如果你沒有stack視窗,可以通過menu -> windows -> call stack (或者 ctrl + d c)將其呼出,開啟call stack後,我們就可以通過右擊滑鼠,選擇go to disassembly進入下面的代碼
static void Main(string[] args) {00000000 push ebp00000001 mov ebp,esp00000003 push edi00000004 push esi00000005 push ebx00000006 sub esp,38h00000009 xor eax,eax0000000b mov dword ptr [ebp-10h],eax0000000e xor eax,eax00000010 mov dword ptr [ebp-1Ch],eax00000013 mov dword ptr [ebp-3Ch],ecx00000016 cmp dword ptr ds:[00912DC8h],00000001d je 000000240000001f call 792B228E00000024 xor esi,esi00000026 xor edi,edi00000028 nopfor (int i = 0; i < 10; i++) {00000029 xor esi,esi0000002b nop0000002c jmp 0000003D0000002e nopConsole.WriteLine("Hello World!");0000002f mov ecx,dword ptr ds:[022B303Ch]00000035 call 785D90740000003a nop}0000003b nopfor (int i = 0; i < 10; i++) {0000003c inc esi0000003d cmp esi,0Ah00000040 setl al00000043 movzx eax,al00000046 mov edi,eax00000048 test edi,edi0000004a jne 0000002E}0000004c nop0000004d lea esp,[ebp-0Ch]00000050 pop ebx00000051 pop esi00000052 pop edi00000053 pop
我們正在查看的就是JIT為我們的代碼產生的native code(本地代碼),我們可以看到簡單的迴圈在native code層次上怎麼啟動並執行,如果你從來沒有研究過native code,這些本來很普通的代碼可能看起來相當的奇怪,讓我們來自己看看這裡發生了什麼
00000029 xor esi,esi
0000002b nop
0000002c jmp 0000003D
上面代碼初始化我們在ESI中的計數器,ESI是一個索引寄存器,可以用來索引數組,你可以看到這裡用了一個很古老的"把戲"來把計數器清0,代碼沒有使用把0值放入寄存器,而是讓寄存器自己異或(xor)自己來達到清0的目的,接下來的一行Nop,意思是"沒有操作",而他們的作用就和他們的名字一樣,什麼也不做,代碼接下來立即跳轉到3D.有時候像這樣的跳轉使得我們的代碼不是自上而下的運行(就象許多進階語言比如c,vb,c#裡面一樣),如果跟著這個跳轉進入這個迴圈的另外一個部分,就可以繼續分析我們的代碼
0000003c inc esi
0000003c後面第一個指令把ESI中的計數器加一(通過register視窗或者按鍵組合ctrl+D R 你可以看到它的值),在第一次迴圈時代碼會跳過這行,因為上面的跳轉指令直接指向了0000003D
0000003d cmp esi,0Ah
00000040 setl al
00000043 movzx eax,al
00000046 mov edi,eax
00000048 test edi,edi
0000004a jne 0000002E
從0000003D開始到4a,代表於迴圈停止值的實際比較和跳轉如果我們沒有達到這個值(i<10),最後一行會跳轉到2e(譯者注:原著這裡為4a,是個筆誤)繼續這個迴圈,也就是迴圈體開始的地方
0000002f mov ecx,dword ptr ds:[022B303Ch]
00000035 call 785D9074
上面的第一條行將會把字串從從記憶體裝在到ECX 寄存器 (這是一個通用寄存器), 一般ECX總是用作把第一個參數傳給方法,在執行個體的方法中,ECX將總是包含this,緊接著是包含第二個參數的EDX,然後是一系列的push,用於把其他參數入棧
下一條語句執行實際的調用。我們待會再來探討怎麼去尋找所調用的方法,但是現在我們可以從VisualStudio給出的原始碼看到,這毫無疑問就是 Console.WriteLine ,代碼接著執行索引的自增,並返回來繼續執行loop迴圈內部的代碼
然而,我們的微不足道的例子中已經產生產生了明顯的浪費。下面是一個例子
00000009 xor eax,eax
0000000b mov dword ptr [ebp-10h],eax
0000000e xor eax,eax
00000010 mov dword ptr [ebp-1Ch],eax
我們在一行裡兩次對EAX置0,這時因為我們正運行在debug模式下,偵錯模式下是不進行最佳化的,換句話說,這段代碼只是被JIT執行,但是卻沒有允許JIT作任何智能最佳化
下面讓我們來看看經過最佳化的代碼:
這裡有一些關於查看最佳化代碼的問題
1)是調試器預設關閉了JIT的最佳化(我自己就曾經在大半夜花了很長時間才意識到自己一直在看沒有被最佳化的代碼)
2)是必須處理"Just My Code"選項對最佳化代碼的影響
我最初在Vance Morrison的文章上看到瞭解決這個問題的辦法(謝謝 Vance,我已經被整個問題困擾了很長一段時間,並最終使用直接查看沒有源碼的原始assemble的方式).
要搞定這個問題,清跟著以下的步驟作
1) 開啟 Tools -> Options -> Debugging -> General
2)確保 ‘Suppress JIT optimization on module load’沒有被選中
3)也確保‘Enable Just My Code’沒有被選中
Vance 也建議我們進入advanced build設定release dll為pdb only,這時我們可以用前面同樣的方式運行這段代碼
用 JIT 看我們的代碼另外一種方式是使用release模式,使用Start the executable without the debugger,再附加visualstudio到進程進行調試.
使用任一方法我們都能讓 JIT 將代碼最佳化了。得到最佳化的代碼如下
for (int i = 0; i < 10; i++) {00000000 push esi00000001 xor esi,esiConsole.WriteLine("Hello World!");00000003 cmp dword ptr ds:[02271084h],00000000a jne 000000160000000c mov ecx,100000011 call 786FC65400000016 mov ecx,dword ptr ds:[02271084h]0000001c mov edx,dword ptr ds:[0227307Ch]00000022 mov eax,dword ptr [ecx]00000024 call dword ptr [eax+000000D8h]for (int i = 0; i < 10; i++) {0000002a add esi,10000002d cmp esi,0Ah00000030 jl 0000000300000032 pop esi}}00000033 ret
哇,這次的代碼比第一次少多了,JIT 最佳化確實工作的很好,這就是為什麼查看實際的反編譯代碼而非IL是這樣重要,因為JIT經常會通過識別IL中的模式來進行最佳化,機敏的讀者可能注意在我們的迴圈的內部事實上產生了更多的密碼。初看起來這是非常可怕的,但其實這說明最佳化器已經協助我們inline了Console.WriteLine方法,實際是節省了很多代碼在接下來的文章中我會談到inline,但是大家先明白這是一個很重要的最佳化
我們這時已經準備好了怎樣在調試器中去欣賞最佳化的和沒有最佳化的代碼,我想這是一個好的開始,下面的幾個文章我會為更深入的瞭解JIT的一般最佳化過程而打好基礎,我們也可以接觸一些工具,看看他們會怎樣協助我們得到更好的代碼
希望能在那裡見你。
原文地址
http://codebetter.com/blogs/gregyoung/archive/2006/06/09/146298.aspx
原文的一些資源:
http://en.wikipedia.org/wiki/X86
http://www.codeguru.com/csharp/.net/net_general/il/article.php/c4635/ IL tutorial
http://burks.brighton.ac.uk/burks/language/asm/asmtut/asm1.htm ASM tutorial