ILy: 宋体"> 循环处理
ILy: 宋体">刘强
cambest@sohu.com
2003ILy: 宋体">年10ILy: 宋体">月22ILy: 宋体">日
ILy: 宋体">上一篇文章我们讨论了ILILy: 宋体">代码的基本运行机制。在这篇文章里,我们将讨论ILILy: 宋体">代码是怎样处理C#ILy: 宋体">中的循环。例子还涉及到数组处理,以及一些新涉及到的指令。虽然已经有人进行过相关问题的研究,我也看过几篇有关文章,不过我认为他们描述得并不是很清楚,所以在这里我借机重新整理成文,希望对大家学习理解.netILy: 宋体">会有所帮助,同时也希望对研究虚拟机机制的有关设计人员有所帮助。
ILy: 宋体">同样,这里也先给出C#ILy: 宋体">代码,然后再让我们详细研究其编译后的ILILy: 宋体">代码。下面是C#ILy: 宋体">代码,它含有三个循环,分别是forILy: 宋体">、whILeILy: 宋体">、foreachILy: 宋体">循环:
public int LoopTest()
{
int i=3;
int j=9;
int s=0;
int k; fILe://ILy: 宋体">以上各条语句定义变量并进行初始化
for(k=0;k<=i;k++)
{
s+=k;
} fILe://forILy: 宋体">循环块
k=0;
whILe(k<j)
{
s+=k;
k++;
int[] array={2,3,4,5,6,7,8,9};
foreach(int a in array)
{
s+=a;
} fILe://foreachILy: 宋体">循环块
return s;
}
ILy: 宋体">在这里,我们要做的是搞清楚C#ILy: 宋体">编译器是把源程序翻译成怎样的ILILy: 宋体">代码以实现循环处理的,或者说如何用ILILy: 宋体">语言实现C#ILy: 宋体">语言中的循环。这对我们深入理解C#ILy: 宋体">语言特性是很有帮助的。当然仅仅这一点还不够,以后我还会介绍更多的有关方面的问题。
ILy: 宋体">首先让我们看看这个函数被编译成什么样的ILILy: 宋体">代码:
.method public hidebysig instance int32 LoopTest() cIL managed
{
// ILy: 宋体">代码大小 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://ILy: 宋体">跟函数返回值类型相同的局部变量,由编译器维护,专门用于存储返回
//ILy: 宋体">值。如果函数为voidILy: 宋体">型,则无此变量。
[7] int32[] CS$00000007$00000001,
fILe://ILy: 宋体">局部变量,存储数组引用,用于foreachILy: 宋体">循环。本例中对应‘arrayILy: 宋体">’数组。
[8] int32 CS$00000008$00000002
fILe://ILy: 宋体">局部变量,存储数组索引。专用于foreachILy: 宋体">循环,由编译器维护。
ILy: 宋体"> )
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_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_0017: ldc.i4.0
IL_0018: stloc.3
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_0027: ldc.i4.8
IL_0028: newarr [mscorlib]System.Int32
fILe://ILy: 宋体">创建长度为8ILy: 宋体">的System.Int32ILy: 宋体">数组。可以看出数组元素被映射到Int32ILy: 宋体">类对象。
IL_002d: dup
IL_002e: ldtoken field valuetype '<PrivateImplementationDetaILs>'/'$$struct0x6000002-1'
'<PrivateImplementationDetaILs>'::'$$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_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_005d: ldloc.2
IL_005e: stloc.s CS$00000003$00000000
IL_0062: ldloc.s CS$00000003$00000000
IL_0064: ret
} // end of method Advanced::LoopTest
ILy: 宋体">关于函数话题如.locals initILy: 宋体">语句等,请参见文章〈函数相关〉。这里我对其中的一些指令做出解释,主要是与本文相关的条件转移指令(b*.sILy: 宋体">)等。其他指令以后我会作适当的介绍。如下所示:
ILy: 宋体">指令
ILy: 宋体">意义
ILy: 宋体">记忆方法(*ILy: 宋体">)
br.s
ILy: 宋体">绝对跳转,相当于jmp
blt.s
ILy: 宋体">小于转
Lower Than
ble.s
ILy: 宋体">小于等于转
Lower or Equals
ldlen
ILy: 宋体">取得数组长度
ldelem.i4
ILy: 宋体">根据索引取得数组项
ILy: 宋体">这里我们可以看到 .locals initILy: 宋体">伪指令给出了同源程序相同变量名称。这是因为在反汇编时,相同目录下有调试信息文件(*.pdbILy: 宋体">),否则的我们看到的结果变量以V_xILy: 宋体">形式(如V_1ILy: 宋体">、V_2ILy: 宋体">等)表示。有关函数局部变量的话题,请参见《函数相关》一文。
ILy: 宋体">如果你有WIN32ILy: 宋体">汇编程序设计经验,可能都熟悉怎样实现循环控制。如,要实现从10ILy: 宋体">加至100ILy: 宋体">的功能,我们可能会这样做:
mov ecx, 100 fILe://ecxILy: 宋体">寄存器存放循环计数
xor eax, eax fILe://ILy: 宋体">给eaxILy: 宋体">和标志寄存器清零
loop: add eax, ecx fILe://ILy: 宋体">实现相加并将结果存eax
dec ecx fILe://ILy: 宋体">计数减一
cpr: cmp ecx, 9 fILe://ILy: 宋体">判断 ecx>=10 ILy: 宋体">或 ecx>9
jg loop fILe://ILy: 宋体">如果判断结果为真(大于)的话,则转loop
ILy: 宋体">这跟高级语言(C/C++/JAVA/C#ILy: 宋体">)不一样,forILy: 宋体">循环中的循环条件在程序首部给出,而顺序执行的低级语言如MASMILy: 宋体">都习惯是在循环末尾测试循环条件的。那么C#ILy: 宋体">编译器又是怎样处理C#ILy: 宋体">循环条件位置与一般汇编中循环条件测试语句位置的不一致,用ILILy: 宋体">来实现循环条件检测并正确实现循环的呢?首先,在这里我要说明,在顺序执行的汇编语言中,测试循环条件是完全可以放在循环首部的。如上例的ILILy: 宋体">版为:
.locals init([0] int32 eax, [1] int32 ecxILy: 宋体">,[2] int32 RET_VAL)
ldc.i4 100
stloc.1 fILe://mov ecx, 100
ldc.i4.0
stloc.0 fILe://xor eax, eax ILy: 宋体">或 move eax, 0
L_0000: ldloc.1
ldc.i4 10 fILe://
blt.s L_0003 // ecx < 10 ? Yes-> jmp L_0001 ILy: 宋体">:No -> go on
L_0001: ldloc.0
ldloc.1
add
stloc.0 fILe://ILy: 宋体">这几句实现 eax=eax+ecx
ldloc.1
ldc.i4.1
sub
stloc.1 fILe://ILy: 宋体">这几句实现 ecx=ecx-1
L_0002: br.s L_0000
L_0003: …
ILy: 宋体">其次,我要说明不这样做的理由。理由有二,其一是破坏了正常逻辑,这一点是从编译器层面上来说的。比如,对于语句if(k<j)ILy: 宋体">,如果将比较放在循环尾部,进行的是(k<jILy: 宋体">)的比较,为真向前跳转转,为假继续执行即可。而放在首部的话,则要进行的是(k>=jILy: 宋体">)的比较,如真则向下跳出循环区域,如假则继续执行;在循环的末尾还要设置绝对跳转语句,以跳转到首部的比较指令处。由(k<j)ILy: 宋体">向(k>=j)ILy: 宋体">转变,对于我们人来说是很简单、直观的事情,可对编译器来说还要做更多的工作才能实现。更何况还有更复杂的布尔表达式呢,如(k>jILy: 宋体">)&& (k>34) || (j<=56)ILy: 宋体">。这就增加了编译器实现的负担——虽然不是很大的负担。而且,因为还增加了跳转语句,给编译器对跳转位置的定位增加了难度。大家知道,汇编器在处理、计算汇编语言中的标号与跳转指令的偏移量时要进行至少两次的扫描,高级语言就更复杂了。因此,采用前一种方法既容易理解,又容易实现。
ILy: 宋体">下面我们来看看例子中三种循环的具体实现。有关ILILy: 宋体">代码的基本运行机制,请参看《ILILy: 宋体">代码底层运行机制》一文。IL_0027ILy: 宋体">到IL_003fILy: 宋体">是进行数组初始化的,比较难懂一点。我们暂且放下,以后我还会介绍。
1.ILy: Times New Roman"> forILy: 宋体">语句
ILy: 宋体">可以看出,程序段中IL_0000ILy: 宋体">到IL_0008ILy: 宋体">是执行变量初始化工作的。从IL_0009ILy: 宋体">开始,就是循环体了。IL_00009ILy: 宋体">是一条直接(绝对)跳转语句,跳转到IL00_13ILy: 宋体">。我们看看这里的内容:
IL_0013: ldloc.3
IL_0014: ldloc.0
ILy: 宋体">加载局部变量3ILy: 宋体">(也即kILy: 宋体">),再加载局部变量0ILy: 宋体">(即jILy: 宋体">)。后面是一条比较转移指令ble.sILy: 宋体">。不难看出,这三条语句用于比较kILy: 宋体">与jILy: 宋体">的大小。如果比较结果为真(小于等于),则转入循环体内(IL_000bILy: 宋体">处),为假则继续执行直接出循环体。过程如行云流水,简洁直观,不多作解释。从这里我们也可以看出,forILy: 宋体">语句是先进行条件测试,后执行循环体的。
... ldloc.3 ldloc.0 ble.s
top
…
top
k
…
top
j
k
…
top
…
loadILy: 宋体">指令将变量逐个加至程序栈。ble.sILy: 宋体">指令进行比较。值得我们注意的是,ble.sILy: 宋体">还要进行清栈操作。不仅是ble.sILy: 宋体">,其他条件转移指令也都是如此。
2.ILy: Times New Roman"> foreachILy: 宋体">语句
foreachILy: 宋体">语句和forILy: 宋体">语句处理过程大致相当。我们感兴趣的是foreachILy: 宋体">怎样处理边界条件。从IL0041ILy: 宋体">开始,就进入了foreachILy: 宋体">循环体。同样一条直接跳转指令把我们带到了IL_0055ILy: 宋体">处,让我们看看这里是什么。
IL_0055: ldloc.s CS$00000008$00000002
IL_0057: ldloc.s CS$00000007$00000001
IL_0059: ldlen
IL_005a: conv.i4
ILy: 宋体">前面我介绍过CS$00000008$00000002ILy: 宋体">是存储数组索引的,CS$00000007$00000001ILy: 宋体">是数组‘arrayILy: 宋体">’的引用。IL0055ILy: 宋体">到IL005bILy: 宋体">的过程操作是这样的:首先向程序栈加载当前索引,再加载数组引用(32ILy: 宋体">位的HashCodeILy: 宋体">)。ldlenILy: 宋体">指令根据数组引用取得数组长度(64ILy: 宋体">位长整型)并将之转换成32ILy: 宋体">为整型,将索引与此长度进行比较。如果小于,则转入循环体继续执行;否则出循环。从这里我们也可以看出,ILILy: 宋体">对数组操作给予了很强的支持,直接为它提供了相应的指令。
3.ILy: Times New Roman"> whILeILy: 宋体">语句和do-whILeILy: 宋体">语句
ILy: 宋体">从例子中可以看出,whILeILy: 宋体">和forILy: 宋体">循环处理方式是一样的。这里没有给出do-whILeILy: 宋体">例子,但是可以想见它跟forILy: 宋体">语句处理是一样的。但是,do-whILeILy: 宋体">循环要注意,在其循环首部没有像forILy: 宋体">和foreachILy: 宋体">循环那样的直接跳转指令跳转到条件测试代码处。因此,不管什么情况,do-whILeILy: 宋体">循环都是至少执行一次的。
ILy: 宋体">在这篇文章中,我介绍了几条有关条件跳转指令,以及C#ILy: 宋体">编译器是怎样处理C#ILy: 宋体">语言中的循环的。其实,本文不能完全算是ILILy: 宋体">底层机制相关文章,但是要深入了解ILILy: 宋体">,这点基础还是必要的。
<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>