提出结论,给出论据(二)

相关链接:
[url=http://rednaxelafx.iteye.com/blog/425595]提出结论,给出论据(一)[/url]

[url=http://rednaxelafx.iteye.com/blog/425595]前一篇[/url]提到Main()方法里的变量j整个消失了。我是如何确定这一点的?

============================================================================

[b][size=small]观察变量j的存在与否[/size][/b]

回忆起原测试代码中变量i与j的关系:它们在每轮for循环中都保持一样的值。在合适的优化下,它们可以看成是同一个变量,因而就不用重复计算i与j的值,只要算其中一个就行。在前一篇中,我们看到for循环里只对一个变量做了累加,那么它到底只是i,还是同时表示了i与j?

为了确认这个问题,我们可以把变量j的初始值变得与i的不一样,那么它们的值就不一样,而无法用同一个变量表示。例如改成0xCAFEBABE:
using System;

namespace ConsoleApplication1 {
class Program {
static void Main( string[ ] args ) {
long j = 0xCAFEBABE;
Console.WriteLine( DateTime.Now.ToString( ) );
for ( long i = 1; i < 10000000000; i++ ) {
j = j + 1;
}
Console.WriteLine( DateTime.Now.ToString( ) );
}
}
}

由JIT生成的目标代码是:
00E70070 push        ebp
00E70071 mov ebp,esp
00E70073 push edi
00E70074 push esi
00E70075 sub esp,20h
00E70078 mov esi,ecx
00E7007A lea edi,[ebp-28h]
00E7007D mov ecx,8
00E70082 xor eax,eax
00E70084 rep stos dword ptr es:[edi]
00E70086 mov ecx,esi
00E70088 lea edi,[ebp-20h]
00E7008B pxor xmm0,xmm0
00E7008F movq mmword ptr [edi],xmm0
00E70093 lea ecx,[ebp-20h]
00E70096 call 792896D0
00E7009B call 792897B0
00E700A0 mov ecx,eax
00E700A2 lea eax,[ebp-20h]
00E700A5 sub esp,8
00E700A8 movq xmm0,mmword ptr [eax]
00E700AC movq mmword ptr [esp],xmm0
00E700B1 lea edx,[ebp-10h]
00E700B4 mov eax,dword ptr [ecx]
00E700B6 call dword ptr [eax+48h]
00E700B9 lea eax,[ebp-10h]
00E700BC sub esp,8
00E700BF movq xmm0,mmword ptr [eax]
00E700C3 movq mmword ptr [esp],xmm0
00E700C8 call 792DDBC0
00E700CD mov edx,eax
00E700CF xor ecx,ecx
00E700D1 call 792DDC30
00E700D6 mov esi,eax
00E700D8 call 792ED2F0
00E700DD mov ecx,eax
00E700DF mov edx,esi
00E700E1 mov eax,dword ptr [ecx]
00E700E3 call dword ptr [eax+000000D8h]
00E700E9 mov esi,1
00E700EE xor edi,edi
00E700F0 add esi,1
00E700F3 adc edi,0
00E700F6 cmp edi,2
00E700F9 jg 00E70105
00E700FB jl 00E700F0
00E700FD cmp esi,540BE400h
00E70103 jb 00E700F0
00E70105 lea edi,[ebp-28h]
00E70108 pxor xmm0,xmm0
00E7010C movq mmword ptr [edi],xmm0
00E70110 lea ecx,[ebp-28h]
00E70113 call 792896D0
00E70118 call 792897B0
00E7011D mov ecx,eax
00E7011F lea eax,[ebp-28h]
00E70122 sub esp,8
00E70125 movq xmm0,mmword ptr [eax]
00E70129 movq mmword ptr [esp],xmm0
00E7012E lea edx,[ebp-18h]
00E70131 mov eax,dword ptr [ecx]
00E70133 call dword ptr [eax+48h]
00E70136 lea eax,[ebp-18h]
00E70139 sub esp,8
00E7013C movq xmm0,mmword ptr [eax]
00E70140 movq mmword ptr [esp],xmm0
00E70145 call 792DDBC0
00E7014A mov edx,eax
00E7014C xor ecx,ecx
00E7014E call 792DDC30
00E70153 mov esi,eax
00E70155 call 792ED2F0
00E7015A mov ecx,eax
00E7015C mov edx,esi
00E7015E mov eax,dword ptr [ecx]
00E70160 call dword ptr [eax+000000D8h]
00E70166 lea esp,[ebp-8]
00E70169 pop esi
00E7016A pop edi
00E7016B pop ebp
00E7016C ret

与前一篇原测试代码生成出来的目标代码对比——两者一模一样。给变量j赋的初始值0xCAFEBABE并没有出现在目标代码中,很好的说明了变量j确实消失了。
变量j在赋值后并没有被用于其它运算(唯一的运算就是用于累加自身),它的值既然不会对程序的其它部分造成任何影响,就可以安全的被优化掉。简单的数据流分析就能发现这点。

============================================================================

[b][size=small]观察变量j的存在的情况[/size][/b]

既然我们知道了原测试代码在实际执行时,Main()中的变量j消失了,那有什么办法能把它留住呢?最简单的办法就是把这个变量输出出来,使变量j的值在运算后用于可见的副作用当中。在原测试代码的最后加一句Console.WriteLine(j);,如下:
using System;

namespace ConsoleApplication1 {
class Program {
static void Main( string[ ] args ) {
long j = 1;
Console.WriteLine( DateTime.Now.ToString( ) );
for ( long i = 1; i < 10000000000; i++ ) {
j = j + 1;
}
Console.WriteLine( DateTime.Now.ToString( ) );
Console.WriteLine( j );
}
}
}

则由JIT生成的目标代码为:
 代码块1:方法头
00E70070 push ebp // 保存帧指针
00E70071 mov ebp,esp // 设置新的帧指针
00E70073 push edi // 这两句保护EDI和ESI寄存器
00E70074 push esi
00E70075 sub esp,30h // 分配局部变量空间
00E70078 mov esi,ecx
00E7007A lea edi,[ebp-38h]
00E7007D mov ecx,8
00E70082 xor eax,eax
00E70084 rep stos dword ptr es:[edi]
00E70086 mov ecx,esi
代码块1结束

代码块2:Program.Main()的方法体

// 为变量j赋初始值
00E70088 mov dword ptr [ebp-10h],1
00E7008F mov dword ptr [ebp-0Ch],0

// 内联开始,System.DateTime.get_Now()
00E70096 lea edi,[ebp-30h]
00E70099 pxor xmm0,xmm0
00E7009D movq mmword ptr [edi],xmm0
00E700A1 lea ecx,[ebp-30h]
00E700A4 call 792896D0 (System.DateTime.get_UtcNow(), mdToken: 060002d2)
00E700A9 call 792897B0 (System.TimeZone.get_CurrentTimeZone(), mdToken: 06000942)
00E700AE mov ecx,eax
00E700B0 lea eax,[ebp-30h]
00E700B3 sub esp,8
00E700B6 movq xmm0,mmword ptr [eax]
00E700BA movq mmword ptr [esp],xmm0
00E700BF lea edx,[ebp-20h]
00E700C2 mov eax,dword ptr [ecx]
00E700C4 call dword ptr [eax+48h] (System.CurrentSystemTimeZone.ToLocalTime(System.DateTime), mdToken: 06000951)
// 内联结束,System.DateTime.get_Now()

// 内联开始,System.DateTime.ToString()
00E700C7 lea eax,[ebp-20h]
00E700CA sub esp,8
00E700CD movq xmm0,mmword ptr [eax]
00E700D1 movq mmword ptr [esp],xmm0
00E700D6 call 792DDBC0 (System.Globalization.DateTimeFormatInfo.get_CurrentInfo(), mdToken: 06002493)
00E700DB mov edx,eax
00E700DD xor ecx,ecx
00E700DF call 792DDC30 (System.DateTimeFormat.Format(System.DateTime, System.String, System.Globalization.DateTimeFormatInfo), mdToken: 06002408)
// 内联结束,System.DateTime.ToString()

// 内联开始,System.Console.WriteLine(System.String)
00E700E4 mov esi,eax
00E700E6 call 792ED2F0 (System.Console.get_Out(), mdToken: 06000772)
00E700EB mov ecx,eax
00E700ED mov edx,esi
00E700EF mov eax,dword ptr [ecx]
00E700F1 call dword ptr [eax+000000D8h] (System.IO.TextWriter+SyncTextWriter.WriteLine(System.String), mdToken: 060036c5)
// 内联结束,System.Console.WriteLine(System.String)

//>> for循环初始段:对变量i赋初始值
00E700F7 mov dword ptr [ebp-18h],1
00E700FE mov dword ptr [ebp-14h],0
//>> for循环体:对变量j累加
00E70105 mov eax,dword ptr [ebp-10h]
00E70108 mov edx,dword ptr [ebp-0Ch]
00E7010B add eax,1
00E7010E adc edx,0
00E70111 mov dword ptr [ebp-10h],eax
00E70114 mov dword ptr [ebp-0Ch],edx
//>> for循环增量段:对变量i累加
00E70117 mov eax,dword ptr [ebp-18h]
00E7011A mov edx,dword ptr [ebp-14h]
00E7011D add eax,1
00E70120 adc edx,0
00E70123 mov dword ptr [ebp-18h],eax
00E70126 mov dword ptr [ebp-14h],edx
//>> for循环条件ver1:
00E70129 cmp dword ptr [ebp-14h],2
00E7012D jg 00E7013A
00E7012F jl 00E70105
//>> for循环条件ver2:
00E70131 cmp dword ptr [ebp-18h],540BE400h
00E70138 jb 00E70105
//>> for循环结束

// 内联开始,System.DateTime.get_Now()
00E7013A lea edi,[ebp-38h]
00E7013D pxor xmm0,xmm0
00E70141 movq mmword ptr [edi],xmm0
00E70145 lea ecx,[ebp-38h]
00E70148 call 792896D0 (System.DateTime.get_UtcNow(), mdToken: 060002d2)
00E7014D call 792897B0 (System.TimeZone.get_CurrentTimeZone(), mdToken: 06000942)
00E70152 mov ecx,eax
00E70154 lea eax,[ebp-38h]
00E70157 sub esp,8
00E7015A movq xmm0,mmword ptr [eax]
00E7015E movq mmword ptr [esp],xmm0
00E70163 lea edx,[ebp-28h]
00E70166 mov eax,dword ptr [ecx]
00E70168 call dword ptr [eax+48h] (System.CurrentSystemTimeZone.ToLocalTime(System.DateTime), mdToken: 06000951)
// 内联结束,System.DateTime.get_Now()

// 内联开始,System.DateTime.ToString()
00E7016B lea eax,[ebp-28h]
00E7016E sub esp,8
00E70171 movq xmm0,mmword ptr [eax]
00E70175 movq mmword ptr [esp],xmm0
00E7017A call 792DDBC0 (System.Globalization.DateTimeFormatInfo.get_CurrentInfo(), mdToken: 06002493)
00E7017F mov edx,eax
00E70181 xor ecx,ecx
00E70183 call 792DDC30 (System.DateTimeFormat.Format(System.DateTime, System.String, System.Globalization.DateTimeFormatInfo), mdToken: 06002408)
// 内联结束,System.DateTime.ToString()

// 内联开始,System.Console.WriteLine(System.String)
00E70188 mov esi,eax
00E7018A call 792ED2F0 (System.Console.get_Out(), mdToken: 06000772)
00E7018F mov ecx,eax
00E70191 mov edx,esi
00E70193 mov eax,dword ptr [ecx]
00E70195 call dword ptr [eax+000000D8h] (System.IO.TextWriter+SyncTextWriter.WriteLine(System.String), mdToken: 060036c5)
// 内联结束,System.Console.WriteLine(System.String)

// 内联开始,System.Console.WriteLine(System.Int64)
00E7019B call 792ED2F0 (System.Console.get_Out(), mdToken: 06000772)
00E701A0 push dword ptr [ebp-0Ch]
00E701A3 push dword ptr [ebp-10h]
00E701A6 mov ecx,eax
00E701A8 mov eax,dword ptr [ecx]
00E701AA call dword ptr [eax+000000C4h] (System.IO.TextWriter+SyncTextWriter.WriteLine(Int64), mdToken: 060036c1)
// 内联结束,System.Console.WriteLine(System.Int64)

代码块2结束

代码块3:方法尾
00E701B0 lea esp,[ebp-8]
00E701B3 pop esi
00E701B4 pop edi
00E701B5 pop ebp
00E701B6 ret
代码块3结束

Program.Main()方法结束


这次我们可以清楚的看到变量j的存在。不仅变量j确实存在于栈上了,受迫于寄存器分配的压力,变量i也从原先直接分配在寄存器ESI和EDI中变为现在也分配在栈上。访问主内存比访问寄存器要慢很多。看看测试时间,会发现加了这么一行就使速度慢了很多,在我的机器上需要2分半钟左右。
就加了一行看似很无辜的代码而已,我们见证了micro-benchmark是如何容易受到各种因素的影响而导致测试结果发生巨大的差异,进而带来误导性的结论。

从这段代码我们可以看出,CLR 2.0对循环中的归纳变量相关的冗余删除做得并不彻底。本来变量i与j还是可以合为一体来计算的,但这里却对它们做了重复计算。这可能是CLR 2.0实现的不足,但更有可能的是[color=green]采取更激进的优化需要更长的编译时间和更多的空间,而JIT的一个重要需求就是要“快”,不能为了产生高效的代码而占用太多时间,否则程序反而会很卡[/color]。

============================================================================

[b][size=small]观察涉及long的方法调用[/size][/b]

顺带提个小细节。CLR 2.0中,大多数方法都是用类似__fastcall的calling convention来调用。这种calling convention规定头两个参数分别放在ECX与EDX中,其余参数与__stdcall一样通过栈来传递;CLR的JIT calling convention跟__fastcall不一样的地方在,前者是把剩余的参数从左向右压栈的,而后者是从右向左

但是留意到上面代码中System.Console.WriteLine(System.Int64)内联进来的代码。首先,这个方法的源码是类似这样的:
public static class Console {
// ...
public static TextWriter Out {
get {
// ...
}
}

public static void WriteLine(long i) {
Console.Out.WriteLine(i);
}
// ...
}

在Console.WriteLine(long)中调用了TextWriter.WriteLine(long)。后者是一个虚方法,意味着它实际的参数列表中第一个参数是一个隐藏的this。根据__fastcall的规定,this应该通过ECX传递,那么要输出的long就应该通过EDX传递了,是这样的吗?仔细看看JIT生成的目标代码:
// 内联开始,System.Console.WriteLine(System.Int64)
00E7019B call 792ED2F0 (System.Console.get_Out(), mdToken: 06000772)
00E701A0 push dword ptr [ebp-0Ch]
00E701A3 push dword ptr [ebp-10h]
00E701A6 mov ecx,eax
00E701A8 mov eax,dword ptr [ecx]
00E701AA call dword ptr [eax+000000C4h] (System.IO.TextWriter+SyncTextWriter.WriteLine(Int64), mdToken: 060036c1)
// 内联结束,System.Console.WriteLine(System.Int64)

可以看到,this确实是通过ECX传递的,但要输出的long型数据却是分两次压到栈上传递,而不是通过EDX传递的。原因很简单:long超过了机器的字长,在EDX里放不下,自然只能从栈上走。[url=http://msdn.microsoft.com/en-us/library/6xa169sk.aspx]__fastcall实际的规定[/url]是:
[quote="MSDN"]The first two DWORD or smaller arguments are passed in ECX and EDX registers; all other arguments are passed right to left.[/quote]
变量j是long型的,是个QWORD,比DWORD大,所以属于“其余参数”,就从栈上传递了。

============================================================================

嗯,关于CLR 2.0与原测试代码的一些“facts”就先写到这里吧。以后要是有机会也可以补充上在64位平台上的相关facts。
前面都只是在关注CLR,下一篇将转到JVM的一边,看看Sun HotSpot VM的一些facts ^ ^
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值