嗯,你目前的计时方式看起来很糟糕。更明智的做法是计算整个循环所需的时间:
Well, the way you're timing things looks pretty nasty to me. It would be much more sensible to just time the whole loop:
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块,只有一个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 机器上尝试了一下。x64 CLR 在这段代码上要比 x86 CLR 快得多(大约快3-4倍),在 x64 下,try/catch块没有明显的差异。