Bem, a maneira como você está cronometrando as coisas me parece bastante desagradável. Seria muito mais sensato apenas cronometrar todo o loop:
var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);
Dessa forma, você não estará à mercê de tempos minúsculos, aritmética de ponto flutuante e erro acumulado.
Depois de fazer essa alteração, verifique se a versão "non-catch" ainda é mais lenta que a versão "catch".
EDIT: Ok, eu já tentei - e estou vendo o mesmo resultado. Muito estranho. Gostaria de saber se o try / catch estava desativando alguns inlining ruins, mas o uso [MethodImpl(MethodImplOptions.NoInlining)]
não ajudou ...
Basicamente, você precisará olhar para o código JITted otimizado em cordbg, suspeito ...
EDIT: Mais algumas informações:
- Colocar o try / catch em torno da
n++;
linha ainda melhora o desempenho, mas não tanto quanto colocá-lo em torno de todo o bloco
- Se você pegar uma exceção específica (
ArgumentException
nos meus testes) ainda é rápido
- Se você imprimir a exceção no bloco catch, ainda é rápido
- Se você repetir a exceção no bloco de captura, fica lento novamente
- Se você usar um bloco finalmente em vez de um bloco de captura, fica lento novamente
- Se você usar um bloco final e um bloco de captura, é rápido
Esquisito...
EDIT: Ok, temos desmontagem ...
Isso está usando o compilador C # 2 e o .NET 2 (32 bits) CLR, desmontando com mdbg (como não tenho cordbg na minha máquina). Eu ainda vejo os mesmos efeitos de desempenho, mesmo sob o depurador. A versão rápida usa um try
bloco em torno de tudo entre as declarações de variável e a declaração de retorno, com apenas um catch{}
manipulador. Obviamente, a versão lenta é a mesma, exceto sem a tentativa / captura. O código de chamada (ou seja, Principal) é o mesmo em ambos os casos e tem a mesma representação de montagem (portanto, não é um problema interno).
Código desmontado para versão rápida:
[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
Código desmontado para a versão lenta:
[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
Em cada caso, os *
shows onde o depurador entrou em um simples "passo a passo".
EDIT: Ok, agora examinei o código e acho que posso ver como cada versão funciona ... e acredito que a versão mais lenta é mais lenta porque usa menos registros e mais espaço de pilha. Para valores pequenos, n
isso é possivelmente mais rápido - mas quando o loop ocupa a maior parte do tempo, é mais lento.
Possivelmente, o bloco try / catch força mais registros a serem salvos e restaurados, então o JIT também os usa para o loop ... o que melhora o desempenho geral. Não está claro se é uma decisão razoável para o JIT não usar tantos registros no código "normal".
Edição: Apenas tentei isso na minha máquina x64. O x64 CLR é muito mais rápido (cerca de 3-4 vezes mais rápido) que o x86 CLR neste código e, em x64, o bloco try / catch não faz uma diferença perceptível.