Bueno, la forma en que estás cronometrando las cosas me parece bastante desagradable. Sería mucho más sensato cronometrar todo el ciclo:
var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);
De esa manera no estás a merced de pequeños tiempos, aritmética de coma flotante y error acumulado.
Después de haber hecho ese cambio, vea si la versión "sin captura" sigue siendo más lenta que la versión "sin captura".
EDITAR: Bien, lo he intentado yo mismo y estoy viendo el mismo resultado. Muy raro. Me preguntaba si el try / catch estaba deshabilitando algunas malas alineaciones, pero usar en su [MethodImpl(MethodImplOptions.NoInlining)]
lugar no ayudó ...
Básicamente, necesitará mirar el código JITted optimizado bajo cordbg, sospecho ...
EDITAR: Un poco más de información:
- Poner el try / catch solo alrededor de la
n++;
línea aún mejora el rendimiento, pero no tanto como ponerlo en todo el bloque
- Si detecta una excepción específica (
ArgumentException
en mis pruebas) aún es rápido
- Si imprime la excepción en el bloque catch, sigue siendo rápido
- Si vuelves a lanzar la excepción en el bloque catch, vuelve a ser lento
- Si usa un bloque finalmente en lugar de un bloque catch, es lento nuevamente
- Si usa un bloque finalmente y un bloque catch, es rápido
Extraño...
EDITAR: Bien, tenemos desmontaje ...
Esto está utilizando el compilador C # 2 y .NET 2 (32 bits) CLR, desmontando con mdbg (ya que no tengo cordbg en mi máquina). Todavía veo los mismos efectos de rendimiento, incluso bajo el depurador. La versión rápida usa un try
bloque alrededor de todo entre las declaraciones de variables y la declaración de retorno, con solo un catch{}
controlador. Obviamente, la versión lenta es la misma, excepto sin el try / catch. El código de llamada (es decir, Principal) es el mismo en ambos casos y tiene la misma representación de ensamblaje (por lo que no es un problema de línea).
Código desmontado para la versión 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 versión 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
En cada caso, el *
programa muestra dónde ingresó el depurador en un simple "paso".
EDITAR: Bien, ahora he revisado el código y creo que puedo ver cómo funciona cada versión ... y creo que la versión más lenta es más lenta porque usa menos registros y más espacio en la pila. Para valores pequeños de n
eso es posiblemente más rápido, pero cuando el ciclo ocupa la mayor parte del tiempo, es más lento.
Posiblemente, el bloque try / catch obliga a guardar y restaurar más registros, por lo que el JIT también los usa para el bucle ... lo que mejora el rendimiento general. No está claro si es una decisión razonable que el JIT no use tantos registros en el código "normal".
EDITAR: Acabo de probar esto en mi máquina x64. El x64 CLR es mucho más rápido (aproximadamente 3-4 veces más rápido) que el x86 CLR en este código, y bajo x64 el bloque try / catch no hace una diferencia notable.