好吧,我对事情的计时方式看起来很讨厌。仅对整个循环进行计时会更明智:
var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);
这样一来,您就不会受制于微小的时序,浮点运算和累积误差。
进行更改后,请查看“非捕获”版本是否仍比“捕获”版本慢。
编辑:好的,我自己尝试过-我看到了相同的结果。很奇怪。我想知道try / catch是否禁用了一些错误的内联,但是使用[MethodImpl(MethodImplOptions.NoInlining)]
替代方法却无济于事...
基本上,您需要在cordbg下查看优化的JITted代码,我怀疑...
编辑:更多信息:
- 仅将try / catch放在
n++;
一行上仍然可以提高性能,但不能像将其放在整个块上那样多
- 如果您
ArgumentException
在我的测试中发现了一个特定的异常,那么它仍然很快
- 如果在catch块中打印异常,它仍然会很快
- 如果将异常重新抛出到catch块中,它又会变慢
- 如果您使用finally块而不是catch块,它又会变慢
- 如果您同时使用finally块和 catch块,则速度很快
奇怪的...
编辑:好的,我们有反汇编...
这是使用C#2编译器和.NET 2(32位)CLR,与mdbg进行反汇编(因为我的机器上没有cordbg)。即使在调试器下,我仍然看到相同的性能效果。快速版本使用一个try
块围绕变量声明和return语句之间的所有内容,仅使用一个catch{}
处理程序。显然,除了没有try / catch之外,慢速版本是相同的。在两种情况下,调用代码(即Main)是相同的,并且具有相同的程序集表示形式(因此,这不是内联的问题)。
快速版本的反汇编代码:
[0000] push ebp
[0001] mov ebp,esp
[0003] push edi
[0004] push esi
[0005] push ebx
[0006] sub esp,1Ch
[0009] xor eax,eax
[000b] mov dword ptr [ebp-20h],eax
[000e] mov dword ptr [ebp-1Ch],eax
[0011] mov dword ptr [ebp-18h],eax
[0014] mov dword ptr [ebp-14h],eax
[0017] xor eax,eax
[0019] mov dword ptr [ebp-18h],eax
*[001c] mov esi,1
[0021] xor edi,edi
[0023] mov dword ptr [ebp-28h],1
[002a] mov dword ptr [ebp-24h],0
[0031] inc ecx
[0032] mov ebx,2
[0037] cmp ecx,2
[003a] jle 00000024
[003c] mov eax,esi
[003e] mov edx,edi
[0040] mov esi,dword ptr [ebp-28h]
[0043] mov edi,dword ptr [ebp-24h]
[0046] add eax,dword ptr [ebp-28h]
[0049] adc edx,dword ptr [ebp-24h]
[004c] mov dword ptr [ebp-28h],eax
[004f] mov dword ptr [ebp-24h],edx
[0052] inc ebx
[0053] cmp ebx,ecx
[0055] jl FFFFFFE7
[0057] jmp 00000007
[0059] call 64571ACB
[005e] mov eax,dword ptr [ebp-28h]
[0061] mov edx,dword ptr [ebp-24h]
[0064] lea esp,[ebp-0Ch]
[0067] pop ebx
[0068] pop esi
[0069] pop edi
[006a] pop ebp
[006b] ret
慢速版本的反汇编代码:
[0000] push ebp
[0001] mov ebp,esp
[0003] push esi
[0004] sub esp,18h
*[0007] mov dword ptr [ebp-14h],1
[000e] mov dword ptr [ebp-10h],0
[0015] mov dword ptr [ebp-1Ch],1
[001c] mov dword ptr [ebp-18h],0
[0023] inc ecx
[0024] mov esi,2
[0029] cmp ecx,2
[002c] jle 00000031
[002e] mov eax,dword ptr [ebp-14h]
[0031] mov edx,dword ptr [ebp-10h]
[0034] mov dword ptr [ebp-0Ch],eax
[0037] mov dword ptr [ebp-8],edx
[003a] mov eax,dword ptr [ebp-1Ch]
[003d] mov edx,dword ptr [ebp-18h]
[0040] mov dword ptr [ebp-14h],eax
[0043] mov dword ptr [ebp-10h],edx
[0046] mov eax,dword ptr [ebp-0Ch]
[0049] mov edx,dword ptr [ebp-8]
[004c] add eax,dword ptr [ebp-1Ch]
[004f] adc edx,dword ptr [ebp-18h]
[0052] mov dword ptr [ebp-1Ch],eax
[0055] mov dword ptr [ebp-18h],edx
[0058] inc esi
[0059] cmp esi,ecx
[005b] jl FFFFFFD3
[005d] mov eax,dword ptr [ebp-1Ch]
[0060] mov edx,dword ptr [ebp-18h]
[0063] lea esp,[ebp-4]
[0066] pop esi
[0067] pop ebp
[0068] ret
在每种情况下,都*
显示了调试器以简单的“逐步进入”方式输入的位置。
编辑:好的,我现在看了一下代码,我想我可以看到每个版本的工作原理了……我相信较慢的版本会更慢,因为它使用较少的寄存器和更多的堆栈空间。对于较小的值n
,可能会更快-但是,如果循环占用了大部分时间,则速度会变慢。
可能try / catch块会强制保存和还原更多的寄存器,因此JIT还将这些寄存器也用于循环...这恰好改善了整体性能。对于JIT,不要在“正常”代码中使用尽可能多的寄存器是否是一个合理的决定,这一点尚不清楚。
编辑:刚在我的x64机器上尝试过。在64位CLR是多快(约3-4倍的速度)比该代码在x86 CLR,并在x64的try / catch块不会使一个显着的差异。