《Effective C#》是基于.Net 1.0版本写的
今天上园子,看到了一篇C#中for和foreach循环的性能 ,对其中的三个
int[] foo = new int[100];
1, foreach (int i in foo)
Console.WriteLine(i.ToString());
2,for(int index=0;index<foo.Length;index++)
Console.WriteLine(foo[index].ToString());
3,int len=foo.Length;
for(int index=0;index<len;index++)
Console.WriteLine(foo[index].ToString());
的解释让我比较怀疑,后来看到作者是看的《Effective C#》,我也翻了一下,但是很快我就翻到前面的前言部分,后来发现《Effective C#》的作者是基于.Net 1.0做的经验总结,现在看来他的解释是有些问题的。
我之所以对新的C# 2.0版本所言甚少,有两个原因。首先,本书中的大部分建议也同样适用于C# 2.0。虽然C# 2.0是一个非常重大的升级版本,但它是建立在C# 1.0基础之上的,且并没有让现如今的建议失效。对于最佳实践有可能发生变化的地方,我已经在文中给出了说明。
第二个原因是现在编写新的C# 2.0功能的高效用法还为时过早。本书的内容是基于我以及我的同事使用C# 1.0的已有经验。我们对于C#2.0中的新功能还没有足够的经验,因而也就不了解能够应用到日常任务中的最佳做法。当在书中编写C# 2.0新功能的高效用法的时机还未成熟时,我并不想误导读者。
测试源码比较简单:
2 static void Foo()
3 {
4 foreach (int i in foo)
5 Console.WriteLine (i);
6 //for (int index = 0; index < foo.Length; index++)
7 // ;
8
9 //int len = foo.Length;
10 //for (int index = 0; index < len; index++)
11 // ;
12 }
13 static void Main(string[] args)
14 {
15 Foo();
16 }
下面是我自己的分析,先看第一个的IL:
2 {
3 .maxstack 2
4 .locals init (
5 [0] int32 i,存储每次循环的数组元素
6 [1] int32[] CS$6$0000,指向要访问的数组
7 [2] int32 CS$7$0001,数组的访问索引
8 [3] bool CS$4$0002)用于判断是否数组越界
9 L_0000: nop
10 L_0001: nop
11 L_0002: ldsfld int32[] Class1::foo将数组foo推上堆栈
12 L_0007: stloc.1 存储在CS$6$0000
13 L_0008: ldc.i4.0 推入一个int32的值0
14 L_0009: stloc.2 存储在CS$7$0001
15 L_000a: br.s L_0014无条件转移到L_0014处
16 L_000c: ldloc.1将CS$6$0000推上堆栈
17 L_000d: ldloc.2将CS$7$0001推上堆栈
18 L_000e: ldelem.i4 根据CS$6$0000将所索引为CS$7$0001的值推向栈顶
19 L_000f: stloc.0 存入i
20 L_0010: ldloc.2将CS$7$0001推上堆栈
21 L_0011: ldc.i4.1 将1推上堆栈
22 L_0012: add CS$7$0001+1
23 L_0013: stloc.2 存储回CS$7$0001
24 L_0014: ldloc.2 将CS$7$0001推上堆栈
25 L_0015: ldloc.1 将CS$6$0000推上堆栈
26 L_0016: ldlen 推入数组的长度
27 L_0017: conv.i4 将栈顶的值装为Int32
28 L_0018: clt 比较CS$7$0001和当前栈顶数组的长度;如果没有越界,则将1推向栈 顶,反之推0
29 L_001a: stloc.3 将比较结果存储在CS$4$0002
30 L_001b: ldloc.3 将CS$4$0002推向栈顶
31 L_001c: brtrue.s L_000c如果CS$4$0002 为true、非空或非零,则跳转到L_001c
32 L_001e: ret
33 }
34
接着是汇编码:
2 0000003f nop
3 00000040 mov eax,dword ptr ds:[02D584C0h] 将数组foo推上堆栈
4 00000045 mov dword ptr [ebp-40h],eax 存储在CS$6$0000
5 00000048 xor edx,edx 推入一个int32的值0
6 0000004a mov dword ptr [ebp-44h],edx 存储在CS$7$0001
7 0000004d nop
8 0000004e jmp 0000006A 无条件转移到L_0014处
9 00000050 mov eax,dword ptr [ebp-44h] 将CS$6$0000推上堆栈
10 00000053 mov edx,dword ptr [ebp-40h] 将CS$7$0001推上堆栈
11 00000056 cmp eax,dword ptr [edx+4] 比较CS$7$0001和当前数组的长度
12 00000059 jb 00000060 无越界则跳转到00000060
13 0000005b call 6B49AD84 这个地方是call越界异常??
14 00000060 mov eax,dword ptr [edx+eax*4+8] 根据CS$6$0000将所索引为CS$7$0001的值推向栈顶
15 00000064 mov dword ptr [ebp-3Ch],eax 存入i
16 00000067 inc dword ptr [ebp-44h] CS$7$0001+1
17 0000006a mov eax,dword ptr [ebp-44h] 将CS$7$0001推上堆栈
18 0000006d mov edx,dword ptr [ebp-40h] 将CS$6$0000推上堆栈
19 00000070 cmp eax,dword ptr [edx+4] 比较CS$7$0001和当前数组的长度
20 00000073 setl al 如果没有越界,则将1推向栈顶,反之推0
21 00000076 movzx eax,al
22 00000079 mov dword ptr [ebp-48h],eax将比较结果存储在CS$4$0002
23 0000007c cmp dword ptr [ebp-48h],0将CS$4$0002推向栈顶,如果CS$4$0002 为true、非空或非零,则跳转到L_001c
24 00000080 jne 00000050
25 70: ;
26
可以看出foreach是会在运行过程中检查数组是否越界的,汇编码的00000050到00000059很好的说明了。
下面是第2个循环的IL:
2 {
3 .maxstack 2
4 .locals init (
5 [0] int32 index,
6 [1] bool CS$4$0000)
7 L_0000: nop
8 L_0001: ldc.i4.0 推入0
9 L_0002: stloc.0 存到index
10 L_0003: br.s L_0009无条件跳转到L_0009
11 L_0005: ldloc.0 推入index
12 L_0006: ldc.i4.1 推入1
13 L_0007: add index+1
14 L_0008: stloc.0 存回index
15 L_0009: ldloc.0推入index
16 L_000a: ldsfld int32[] Class1::foo 推入数组foo
17 L_000f: ldlen推入数组的长度
18 L_0010: conv.i4 转换为Int32
19 L_0011: clt比较index和当前栈顶数组的长度;如果没有越界,则将1推向栈顶,反之推0
20 L_0013: stloc.1 结果存入CS$4$0000
21 L_0014: ldloc.1 推入CS$4$0000
22 L_0015: brtrue.s L_0005 CS$4$0000为真则跳转到L_0005
23 L_0017: ret
24 }
25
汇编码:
2 00000035 xor edx,edx推入0
3 00000037 mov dword ptr [ebp-3Ch],edx存到index
4 0000003a nop
5 0000003b jmp 00000040无条件跳转到L_0009
6 0000003d inc dword ptr [ebp-3Ch]
7 00000040 mov eax,dword ptr [ebp-3Ch] 推入index
8 00000043 mov edx,dword ptr ds:[02DA84C0h] 推入数组foo
9 00000049 cmp eax,dword ptr [edx+4] 比较index和当前数组的长度
10 0000004c setl al 如果没有越界,则将1推向栈顶,反之推0
11 0000004f movzx eax,al
12 00000052 mov dword ptr [ebp-40h],eax 将比较结果存入CS$4$0000
13 00000055 cmp dword ptr [ebp-40h],0将CS$4$0000推向栈顶,如果CS$4$0000为true、非空或非零,则跳转到L_0005
14
15 00000059 jne 0000003D
16 70: ;
17 71: }
18
for循环生成的代码则比较简单,而且只是检查越界一次。
第三个循环则和第二个没什么区别,就是多了一个临时变量,将IL和汇编码放在一起:
2 {
3 .maxstack 2
4 .locals init (
5 [0] int32 len,
6 [1] int32 index,
7 [2] bool CS$4$0000)
8 L_0000: nop
9 L_0001: ldsfld int32[] Class1::foo
10 L_0006: ldlen
11 L_0007: conv.i4
12 L_0008: stloc.0
13 L_0009: ldc.i4.0
14 L_000a: stloc.1
15 L_000b: br.s L_0011
16 L_000d: ldloc.1
17 L_000e: ldc.i4.1
18 L_000f: add
19 L_0010: stloc.1
20 L_0011: ldloc.1
21 L_0012: ldloc.0
22 L_0013: clt
23 L_0015: stloc.2
24 L_0016: ldloc.2
25 L_0017: brtrue.s L_000d
26 L_0019: ret
27 }
28
29 69: int len = foo.Length;
30 0000003a mov eax,dword ptr ds:[02C084C0h]
31 0000003f mov eax,dword ptr [eax+4]
32 00000042 mov dword ptr [ebp-3Ch],eax
33 70: for (int index = 0; index < len; index++)
34 00000045 xor edx,edx
35 00000047 mov dword ptr [ebp-40h],edx
36 0000004a nop
37 0000004b jmp 00000050
38 0000004d inc dword ptr [ebp-40h]
39 00000050 mov eax,dword ptr [ebp-40h]
40 00000053 cmp eax,dword ptr [ebp-3Ch]
41 00000056 setl al
42 00000059 movzx eax,al
43 0000005c mov dword ptr [ebp-44h],eax
44 0000005f cmp dword ptr [ebp-44h],0
45 00000063 jne 0000004D
46 71: ;
47
我是在.Net3.5上运行的代码,没有实际测试1.0环境,也确实找不到了,如果错误之处,请大家指正。
PS:偶然的机会发现数组的长度不是计算的,而是在数组的首地址+4的位置存储的,大家可以用windbg看一看。