Beh, il modo in cui cronometri le cose mi sembra piuttosto brutto. Sarebbe molto più sensato temporizzare l'intero ciclo:
var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);
In questo modo non sei in balia di piccoli tempi, aritmetica in virgola mobile e errore accumulato.
Dopo aver apportato tale modifica, vedere se la versione "non catch" è ancora più lenta della versione "catch".
EDIT: Okay, l'ho provato da solo - e sto vedendo lo stesso risultato. Molto strano. Mi chiedevo se il tentativo / cattura disabilitasse un cattivo allineamento, ma l'utilizzo [MethodImpl(MethodImplOptions.NoInlining)]
invece non ha aiutato ...
Fondamentalmente dovrai guardare il codice JITted ottimizzato sotto cordbg, sospetto ...
EDIT: qualche altra informazione:
- Mettere il try / catch intorno alla
n++;
linea migliora ancora le prestazioni, ma non tanto quanto metterlo attorno all'intero blocco
- Se si rileva un'eccezione specifica (
ArgumentException
nei miei test) è ancora veloce
- Se si stampa l'eccezione nel blocco catch è ancora veloce
- Se ripeti l'eccezione nel blocco catch è di nuovo lento
- Se usi un blocco finally invece di un blocco catch, è di nuovo lento
- Se usi un blocco finally e un blocco catch, è veloce
Strano...
EDIT: Okay, abbiamo lo smontaggio ...
Questo sta usando il compilatore C # 2 e .NET 2 (32-bit) CLR, disassemblando con mdbg (poiché non ho cordbg sulla mia macchina). Vedo ancora gli stessi effetti sulle prestazioni, anche sotto il debugger. La versione veloce utilizza un try
blocco attorno a tutto tra le dichiarazioni delle variabili e l'istruzione return, con solo un catch{}
gestore. Ovviamente la versione lenta è la stessa tranne senza il try / catch. Il codice chiamante (ovvero Main) è lo stesso in entrambi i casi e ha la stessa rappresentazione di assembly (quindi non è un problema in linea).
Codice smontato per la versione veloce:
[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
Codice smontato per versione 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
In ogni caso, *
mostra dove il debugger è entrato in un semplice "step-into".
EDIT: Okay, ora ho controllato il codice e penso di poter vedere come funziona ogni versione ... e credo che la versione più lenta sia più lenta perché utilizza meno registri e più spazio nello stack. Per piccoli valori n
è forse più veloce, ma quando il ciclo occupa gran parte del tempo, è più lento.
Forse il blocco try / catch forza il salvataggio e il ripristino di più registri, quindi JIT usa anche quelli per il loop ... il che migliora le prestazioni complessive. Non è chiaro se sia una decisione ragionevole per JIT non utilizzare altrettanti registri nel codice "normale".
EDIT: Ho appena provato questo sulla mia macchina x64. Il CLR x64 è molto più veloce (circa 3-4 volte più veloce) del CLR x86 su questo codice e in x64 il blocco try / catch non fa alcuna differenza evidente.