IL程式碼底層執行機制之迴圈處理 (轉)

worldblog發表於2007-08-17
IL程式碼底層執行機制之迴圈處理 (轉)[@more@] microsoft FrontPage 4.0">

  IL程式碼底層執行機制之

        迴圈處理

                         劉強

                     cambest@sohu.com

                      年10月22日

上一篇文章我們討論了IL程式碼的基本執行機制。在這篇文章裡,我們將討論IL程式碼是怎樣處理中的迴圈。例子還涉及到陣列處理,以及一些新涉及到的指令。雖然已經有人進行過相關問題的研究,我也看過幾篇有關文章,不過我認為他們描述得並不是很清楚,所以在這裡我藉機重新整理成文,希望對大家學習理解會有所幫助,同時也希望對研究虛擬機器機制的有關設計人員有所幫助。

同樣,這裡也先給出C#程式碼,然後再讓我們詳細研究其編譯後的IL程式碼。下面是C#程式碼,它含有三個迴圈,分別是for、while、foreach迴圈:

public int LTest()

{

  int i=3;

  int j=9;

  int s=0;

  int k;  file://以上各條語句定義變數並進行初始化

 

  for(k=0;k<=i;k++)

  {

  s+=k;

  }  file://for迴圈塊

  k=0;

 

  while(k

  {

  s+=k;

  k++;

  }  file://while迴圈塊

 

  int[] array={2,3,4,5,6,7,8,9};

  foreach(int a in array)

  {

  s+=a;

  }  file://foreach迴圈塊

  return s;

}

在這裡,我們要做的是搞清楚C#是把源翻譯成怎樣的IL程式碼以實現迴圈處理的,或者說如何用IL語言實現C#語言中的迴圈。這對我們深入理解C#語言特性是很有幫助的。當然僅僅這一點還不夠,以後我還會介紹更多的有關方面的問題。

  首先讓我們看看這個被編譯成什麼樣的IL程式碼:

.method public hidebysig instance int32  LoopTest() cil managed

{

  // 程式碼大小  101 (0x65)

  .maxstack  3

  .locals init ([0] int32 i,

  [1] int32 j,

  [2] int32 s,

  [3] int32 k,

  [4] int32[] 'array',

  [5] int32 a,

  [6] int32 CS$00000003$00000000,

  file://跟函式返回值型別相同的區域性變數,由編譯器維護,專門用於返回

//值。如果函式為void型,則無此變數。

 [7] int32[] CS$00000007$00000001,

    file://區域性變數,儲存陣列引用,用於foreach迴圈。本例中對應‘array’陣列。

  [8] int32 CS$00000008$00000002

    file://區域性變數,儲存陣列。專用於foreach迴圈,由編譯器維護。

  )

  IL_0000:  ldc.i4.3

  IL_0001:  stloc.0

  IL_0002:  ldc.i4.s  9

  IL_0004:  stloc.1

  IL_0005:  ldc.i4.0

  IL_0006:  stloc.2

  IL_0007:  ldc.i4.0

  IL_0008:  stloc.3

  IL_0009:  br.s  IL_0013

  IL_000b:  ldloc.2

  IL_000c:  ldloc.3

  IL_000d:  add

  IL_000e:  stloc.2

  IL_000f:  ldloc.3

  IL_0010:  ldc.i4.1

  IL_0011:  add

  IL_0012:  stloc.3

  IL_0013:  ldloc.3

  IL_0014:  ldloc.0

  IL_0015:  ble.s  IL_000b

  IL_0017:  ldc.i4.0

  IL_0018:  stloc.3

  IL_0019:  br.s  IL_0023

  IL_001b:  ldloc.2

  IL_001c:  ldloc.3

  IL_001d:  add

  IL_001e:  stloc.2

  IL_001f:  ldloc.3

  IL_0020:  ldc.i4.1

  IL_0021:  add

  IL_0022:  stloc.3

  IL_0023:  ldloc.3

  IL_0024:  ldloc.1

  IL_0025:  blt.s  IL_001b

  IL_0027:  ldc.i4.8

  IL_0028:  newarr  [mrlib]System.Int32

  file://建立長度為8的System.Int32陣列。可以看出陣列元素被對映到Int32類。

 IL_002d:  dup

  IL_002e: ldtoken field valuetype ''/'$$struct0x6000002-1'

  ''::'$$method0x6000002-1'

  IL_0033: call void [mscorlib] System.Runtime.CompilerServices.RuntimeHelpers::

   InitializeArray(class[mscorlib]System.Array, valuetype  [mscorlib] System.RuntimeFieldHandle)

  IL_0038:  stloc.s  'array'

  IL_003a:  ldloc.s  'array'

  IL_003c:  stloc.s  CS$00000007$00000001

  IL_003e:  ldc.i4.0

  IL_003f:  stloc.s  CS$00000008$00000002

  IL_0041:  br.s  IL_0055

  IL_0043:  ldloc.s  CS$00000007$00000001

  IL_0045:  ldloc.s  CS$00000008$00000002

  IL_0047:  ldelem.i4

  IL_0048:  stloc.s  a

  IL_004a:  ldloc.2

  IL_004b:  ldloc.s  a

  IL_004d:  add

  IL_004e:  stloc.2

  IL_004f:  ldloc.s  CS$00000008$00000002

  IL_0051:  ldc.i4.1

  IL_0052:  add

  IL_0053:  stloc.s  CS$00000008$00000002

  IL_0055:  ldloc.s  CS$00000008$00000002

  IL_0057:  ldloc.s  CS$00000007$00000001

  IL_0059:  ldlen

  IL_005a:  conv.i4

  IL_005b:  blt.s  IL_0043

  IL_005d:  ldloc.2

  IL_005e:  stloc.s  CS$00000003$00000000

  IL_0060:  br.s  IL_0062

  IL_0062:  ldloc.s  CS$00000003$00000000

  IL_0064:  ret

} // end of method Advanced::LoopTest

關於函式話題如.locals init語句等,請參見文章〈函式相關〉。這裡我對其中的一些指令做出解釋,主要是與本文相關的條件轉移指令(b*.s)等。其他指令以後我會作適當的介紹。如下所示:

  指令

  意義

  記憶方法(*)

  br.s

絕對跳轉,相當於jmp

 

  blt.s

小於轉

  Lower Than

  ble.s

小於等於轉

  Lower or Equals

  ldlen

取得陣列長度

 

  ldelem.i4

根據索引取得陣列項

 

 

這裡我們可以看到 .locals init偽指令給出了同源程式相同變數名稱。這是因為在反時,相同目錄下有資訊(*.p),否則的我們看到的結果變數以V_x形式(如V_1、V_2等)表示。有關函式區域性變數的話題,請參見《函式相關》一文。

  如果你有彙編,可能都熟悉怎樣實現迴圈控制。如,要實現從10加至100的功能,我們可能會這樣做:

  mov  ecx,  100  file://ecx暫存器存放迴圈計數

  xor  eax,  eax  file://給eax和標誌暫存器清零

loop:  add  eax,  ecx  file://實現相加並將結果存eax

  dec  ecx  file://計數減一

cpr:  cmp  ecx,  9  file://判斷 ecx>=10 或 ecx>9

  jg  loop  file://如果判斷結果為真(大於)的話,則轉loop

這跟高階語言(C/C++//C#)不一樣,for迴圈中的迴圈條件在程式首部給出,而順序的低階語言如MASM都習慣是在迴圈末尾測試迴圈條件的。那麼C#編譯器又是怎樣處理C#迴圈條件位置與一般彙編中迴圈條件測試語句位置的不一致,用IL來實現迴圈條件檢測並正確實現迴圈的呢?首先,在這裡我要說明,在順序執行的組合語言中,測試迴圈條件是完全可以放在迴圈首部的。如上例的IL版為:

  .locals init([0] int32 eax, [1] int32 ecx,[2] int32 RET_VAL)

  ldc.i4 100

  stloc.1  file://mov ecx, 100

  ldc.i4.0

  stloc.0  file://xor eax, eax 或 move eax, 0

L_0000:  ldloc.1

  ldc.i4 10    file://

  blt.s  L_0003  // ecx < 10 ? Yes-> jmp L_0001 :No -> go on

L_0001:  ldloc.0

  ldloc.1

  add

  stloc.0  file://這幾句實現 eax=eax+ecx

  ldloc.1

  ldc.i4.1

  sub 

  stloc.1  file://這幾句實現 ecx=ecx-1

L_0002:  br.s  L_0000

L_0003:  …

其次,我要說明不這樣做的理由。理由有二,其一是破壞了正常邏輯,這一點是從編譯器層面上來說的。比如,對於語句if(k=j)的比較,如真則向下跳出迴圈區域,如假則繼續執行;在迴圈的末尾還要設定絕對跳轉語句,以跳轉到首部的比較指令處。由(k=j)轉變,對於我們人來說是很簡單、直觀的事情,可對編譯器來說還要做更多的工作才能實現。更何況還有更復雜的布林呢,如(k>j)&& (k>34)  || (j<=56)。這就增加了編譯器實現的負擔——雖然不是很大的負擔。而且,因為還增加了跳轉語句,給編譯器對跳轉位置的定位增加了難度。大家知道,彙編器在處理、計算組合語言中的標號與跳轉指令的偏移量時要進行至少兩次的掃描,高階語言就更復雜了。因此,採用前一種方法既容易理解,又容易實現。

  下面我們來看看例子中三種迴圈的具體實現。有關IL程式碼的基本執行機制,請參看《IL程式碼底層執行機制》一文。IL_0027到IL_003f是進行陣列初始化的,比較難懂一點。我們暫且放下,以後我還會介紹。

1.  for語句

可以看出,程式段中IL_0000到IL_0008是執行變數初始化工作的。從IL_0009開始,就是迴圈體了。IL_00009是一條直接(絕對)跳轉語句,跳轉到IL00_13。我們看看這裡的內容:

IL_0013:  ldloc.3

   IL_0014:  ldloc.0

   IL_0015:  ble.s  IL_000b

載入區域性變數3(也即k),再載入區域性變數0(即j)。後面是一條比較轉移指令ble.s。不難看出,這三條語句用於比較k與j的大小。如果比較結果為真(小於等於),則轉入迴圈體內(IL_000b處),為假則繼續執行直接出迴圈體。過程如行雲流水,簡潔直觀,不多作解釋。從這裡我們也可以看出,for語句是先進行條件測試,後執行迴圈體的。

 

 ...  ldloc.3    ldloc.0   ble.s

 

top

 

 

  top

  k

 …

 

  top

  j

  k

 …

 

 

  top

 …

 

 

   

 

 

 

 load指令將變數逐個加至程式棧。ble.s指令進行比較。值得我們注意的是,ble.s還要進行清棧操作。不僅是ble.s,其他條件轉移指令也都是如此。

2.  foreach語句

foreach語句和for語句處理過程大致相當。我們感興趣的是foreach怎樣處理邊界條件。從IL0041開始,就進入了foreach迴圈體。同樣一條直接跳轉指令把我們帶到了IL_0055處,讓我們看看這裡是什麼。

IL_0055:  ldloc.s  CS$00000008$00000002

     IL_0057:  ldloc.s  CS$00000007$00000001

  IL_0059:  ldlen

    IL_005a:  conv.i4

   IL_005b:  blt.s  IL_0043

前面我介紹過CS$00000008$00000002是儲存陣列索引的,CS$00000007$00000001是陣列‘array’的引用。IL0055到IL005b的過程操作是這樣的:首先向程式棧載入當前索引,再載入陣列引用(32位的HashCode)。ldlen指令根據陣列引用取得陣列長度(64位長整型)並將之轉換成32為整型,將索引與此長度進行比較。如果小於,則轉入迴圈體繼續執行;否則出迴圈。從這裡我們也可以看出,IL對陣列操作給予了很強的支援,直接為它提供了相應的指令。

3.  while語句和do-while語句

從例子中可以看出,while和for迴圈處理方式是一樣的。這裡沒有給出do-while例子,但是可以想見它跟for語句處理是一樣的。但是,do-while迴圈要注意,在其迴圈首部沒有像for和foreach迴圈那樣的直接跳轉指令跳轉到條件測試程式碼處。因此,不管什麼情況,do-while迴圈都是至少執行一次的。

 

  在這篇文章中,我介紹了幾條有關條件跳轉指令,以及C#編譯器是怎樣處理C#語言中的迴圈的。其實,本文不能完全算是IL底層機制相關文章,但是要深入瞭解IL,這點基礎還是必要的。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-963560/,如需轉載,請註明出處,否則將追究法律責任。

相關文章