从如此简单的代码谈起
2014-09-19 19:40 Franz 阅读(1376) 评论(3) 编辑 收藏 举报从如此简单的代码谈起
事情缘起, 前一段时间在公司的技术群里讨论以下方法那个更快.
public class Demo{
private int count;
const int maxNum = int.MaxValue;
public void Run1()
{
for(int i =0 ; i < maxNum; i++)
{
this.count++;
}
Console.WriteLine(this.count);
Console.ReadKey();
}
public void Run2()
{
int temp = 0;
for(int i = 0 ; i < maxNum; i++)
{
temp ++;
}
this.count = temp;
Console.WriteLine(this.count);
Console.ReadKey();
}
这个很多人都能正确的感知到是Run2方法更快一点, 但是为何呢?
我们先看一下两个方法的IL指令吧。
Demo.Run1:
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0 // i
IL_0003: br.s IL_0019
IL_0005: nop
IL_0006: ldarg.0
IL_0007: dup
IL_0008: ldfld UserQuery+Demo.count
IL_000D: ldc.i4.1
IL_000E: add
IL_000F: stfld UserQuery+Demo.count
IL_0014: nop
IL_0015: ldloc.0 // i
IL_0016: ldc.i4.1
IL_0017: add
IL_0018: stloc.0 // i
IL_0019: ldloc.0 // i
IL_001A: ldc.i4 FF FF FF 7F
IL_001F: clt
IL_0021: stloc.1 // CS$4$0000
IL_0022: ldloc.1 // CS$4$0000
IL_0023: brtrue.s IL_0005
IL_0025: ldarg.0
IL_0026: ldfld UserQuery+Demo.count
IL_002B: call System.Console.WriteLine
IL_0030: nop
IL_0031: call System.Console.ReadKey
IL_0036: pop
IL_0037: ret
Demo.Run2:
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0 // temp
IL_0003: ldc.i4.0
IL_0004: stloc.1 // i
IL_0005: br.s IL_0011
IL_0007: nop
IL_0008: ldloc.0 // temp
IL_0009: ldc.i4.1
IL_000A: add
IL_000B: stloc.0 // temp
IL_000C: nop
IL_000D: ldloc.1 // i
IL_000E: ldc.i4.1
IL_000F: add
IL_0010: stloc.1 // i
IL_0011: ldloc.1 // i
IL_0012: ldc.i4 FF FF FF 7F
IL_0017: clt
IL_0019: stloc.2 // CS$4$0000
IL_001A: ldloc.2 // CS$4$0000
IL_001B: brtrue.s IL_0007
IL_001D: ldarg.0
IL_001E: ldloc.0 // temp
IL_001F: stfld UserQuery+Demo.count
IL_0024: ldarg.0
IL_0025: ldfld UserQuery+Demo.count
IL_002A: call System.Console.WriteLine
IL_002F: nop
IL_0030: call System.Console.ReadKey
IL_0035: pop
IL_0036: ret
Demo..ctor:
IL_0000: ldarg.0
IL_0001: call System.Object..ctor
IL_0006: ret
可以看到大致是ldfld指令跟ldloc指令的差异。 因为这些都是il指令, 所以不好衡量那个更快一点。
il指令在运行时才会被转换到机器码, 让我们看一下他们的从汇编的角度是如何看到的
注, 用Windbg加载程序运行完毕, 并使用!U命令可以查看JIT generated code。 当然u命令依然可以使用. 这里使用的是!U的好处是可以清楚还原一下call指令。
0:003> !U 00007ff963450380
Normal JIT generated code
Demo.Run2()
Begin 00007ff963450380, size 4d
>>> 00007ff9`63450380 53 push rbx
00007ff9`63450381 4883ec30 sub rsp,30h
00007ff9`63450385 488bd9 mov rbx,rcx
00007ff9`63450388 33c0 xor eax,eax
00007ff9`6345038a 8bc8 mov ecx,eax
00007ff9`6345038c 0f1f4000 nop dword ptr [rax]
00007ff9`63450390 83c101 add ecx,1
00007ff9`63450393 83c001 add eax,1
00007ff9`63450396 3dffffff7f cmp eax,7FFFFFFFh
00007ff9`6345039b 7cf3 jl 00007ff9`63450390
00007ff9`6345039d 894b08 mov dword ptr [rbx+8],ecx
00007ff9`634503a0 8b5b08 mov ebx,dword ptr [rbx+8]
00007ff9`634503a3 e8b816aa5e call mscorlib_ni+0x391a60 (00007ff9`c1ef1a60) (System.Console.get_Out(), mdToken: 06000776)
00007ff9`634503a8 4c8bd8 mov r11,rax
00007ff9`634503ab 498b03 mov rax,qword ptr [r11]
00007ff9`634503ae 8bd3 mov edx,ebx
00007ff9`634503b0 498bcb mov rcx,r11
00007ff9`634503b3 ff9068010000 call qword ptr [rax+168h]
00007ff9`634503b9 33d2 xor edx,edx
00007ff9`634503bb 488d4c2420 lea rcx,[rsp+20h]
00007ff9`634503c0 e8bbd5035f call mscorlib_ni+0x92d980 (00007ff9`c248d980) (System.Console.ReadKey(Boolean), mdToken: 060007aa)
00007ff9`634503c5 90 nop
00007ff9`634503c6 4883c430 add rsp,30h
00007ff9`634503ca 5b pop rbx
00007ff9`634503cb f3c3 rep ret
0:003> !U 00007ff963450310
Normal JIT generated code
Demo.Run1()
Begin 00007ff963450310, size 51
>>> 00007ff9`63450310 53 push rbx
00007ff9`63450311 4883ec30 sub rsp,30h
00007ff9`63450315 33d2 xor edx,edx
00007ff9`63450317 660f1f840000000000 nop word ptr [rax+rax]
00007ff9`63450320 8b4108 mov eax,dword ptr [rcx+8]
00007ff9`63450323 83c001 add eax,1
00007ff9`63450326 894108 mov dword ptr [rcx+8],eax
00007ff9`63450329 83c201 add edx,1
00007ff9`6345032c 81faffffff7f cmp edx,7FFFFFFFh
00007ff9`63450332 7cec jl 00007ff9`63450320
00007ff9`63450334 8b5908 mov ebx,dword ptr [rcx+8]
00007ff9`63450337 e82417aa5e call mscorlib_ni+0x391a60 (00007ff9`c1ef1a60) (System.Console.get_Out(), mdToken: 06000776)
00007ff9`6345033c 4c8bd8 mov r11,rax
00007ff9`6345033f 498b03 mov rax,qword ptr [r11]
00007ff9`63450342 8bd3 mov edx,ebx
00007ff9`63450344 498bcb mov rcx,r11
00007ff9`63450347 ff9068010000 call qword ptr [rax+168h]
00007ff9`6345034d 33d2 xor edx,edx
00007ff9`6345034f 488d4c2420 lea rcx,[rsp+20h]
00007ff9`63450354 e827d6035f call mscorlib_ni+0x92d980 (00007ff9`c248d980) (System.Console.ReadKey(Boolean), mdToken: 060007aa)
00007ff9`63450359 90 nop
00007ff9`6345035a 4883c430 add rsp,30h
00007ff9`6345035e 5b pop rbx
00007ff9`6345035f f3c3 rep ret
我们只关心Console调用前的指令就可以了。 细细对比一下差异可以看到。
Run2 做加法操作主要是考的这两个add指令, 分别操作了两个寄存器, 一个eax是i++, 一个ecx是temp++;
00007ff9`63450390 83c101 add ecx,1
00007ff9`63450393 83c001 add eax,1
Run1做加法的主要的指令如下, 可以看到多了两条mov操作eax寄存器.
00007ff9`63450320 8b4108 mov eax,dword ptr [rcx+8]
00007ff9`63450323 83c001 add eax,1
00007ff9`63450326 894108 mov dword ptr [rcx+8],eax
00007ff9`63450329 83c201 add edx,1
备注: http://zh.wikipedia.org/wiki/X86调用约定 可以看具体在寄存器的约定.
mov操作从rcx寄存器+8的位置上取了个数(这个是托管堆上对象所在地方). 第一步从存储中取数到eax寄存器, 第二步, 对寄存器加1, 第三步, 将寄存器的值写回到存储器. 第四句是i++操作先忽略.
为了满足大家的好奇心, 我将对象的布局打一下.可以看到offset 8 的地方存放着我们要的count值.
0:003> !do 000000000253baf0
Name: Demo
MethodTable: 00007ff9632f3c20
EEClass: 00007ff9634422d8
Size: 24(0x18) bytes
(C:\Users\cuiweifu\AppData\Local\Temp\SnippetCompilerTemp\8d61e6aa-2c9c-41b4-b8a5-174f83e44e0a\output.exe)
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9c1f9f060 4000002 8 System.Int32 1 instance 2147483647 count
好了,我们知道以上操作细节了, 让我们回顾一下计算机组成原理中的一些基本常识.
离CPU的执行速率越高, L1 > L2 > L3 > 内存 > 硬盘 > 其他外设.
寄存器是CPU的一部分 (忘记的参看这里 http://zh.wikipedia.org/wiki/寄存器 ), 所以寄存器的操作是最快的. 而对缓存的访问取决在哪一个层次上命中.
就本例来说,这是寄存器 PK L1的测试结果.
这里需要提到的一点是这个加操作是在L1做的, 所以内存中真正的值是靠CPU刷新到内存中的. 如果是在多线程的环境下同时的调用Run1方法, 那么输出值是不确定的.
我们.NET中为了避免这件事情, 使用的方法是
System.Threading.Interlocked.Increment(ref this.count);
这个最终转换成为CPU的一个原子指令 (我记得不同的CPU上指令名称不一样,所以这里就不提指令的名字了). 这个指令能保证原子性的更新这个值.
可以测试这种方法虽然原子,但是比Run1方法还会慢一点. 具体的原因感兴趣的同学自己研究吧.
题外话: 首先感谢杨杰同学的催促,我才努力挤出点时间写这篇文章, 虽然文章是粗枝大叶的描述了一下希望对大家的理解有所帮助.
我本想从编译原理的角度来谈一些事情, 从CPU指令以及操作系统的视角上看这个问题, 但是我发现以我拙笨的描述能力很难在一篇blog中说清, 而且需要提及的东西太多, 有太多相关概念需要了解,所以也只能浅尝辄止了. 如果大家喜欢一些原理细节的东西欢迎跟我探讨.
ps: 公司正在招聘.NET程序员, 前端工程师, 数据库工程师,如果有兴趣请私信我