Sposób, w jaki mierzysz czas, wydaje mi się dość paskudny. O wiele rozsądniej byłoby po prostu zmierzyć całą pętlę:
var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);
W ten sposób nie jesteś na łasce drobnych czasów, arytmetyki zmiennoprzecinkowej i skumulowanego błędu.
Po dokonaniu tej zmiany sprawdź, czy wersja „non-catch” jest wolniejsza niż wersja „catch”.
EDYCJA: OK, sam tego próbowałem - i widzę ten sam rezultat. Bardzo dziwne. Zastanawiałem się, czy try / catch wyłącza jakieś złe wstawianie, ale użycie [MethodImpl(MethodImplOptions.NoInlining)]
zamiast tego nie pomogło ...
Zasadniczo musisz spojrzeć na zoptymalizowany kod JITted pod cordbg, podejrzewam ...
EDYCJA: Kilka dodatkowych informacji:
- Umieszczenie try / catch na samej
n++;
linii wciąż poprawia wydajność, ale nie tak bardzo, jak na całym bloku
- Jeśli złapiesz określony wyjątek (
ArgumentException
w moich testach), to wciąż jest szybki
- Jeśli wydrukujesz wyjątek w bloku catch, nadal będzie on szybki
- Jeśli ponownie wrzucisz wyjątek w bloku catch, będzie on znowu wolny
- Jeśli użyjesz bloku w końcu zamiast bloku przechwytywania, znów będzie on wolny
- Jeśli użyjesz bloku wreszcie, a także bloku catch, jest to szybkie
Dziwne...
EDYCJA: OK, mamy demontaż ...
Korzysta z kompilatora C # 2 i CLR .NET 2 (32-bit), dezasembluje się z mdbg (ponieważ nie mam cordbg na moim komputerze). Nadal widzę te same efekty wydajnościowe, nawet pod debuggerem. Wersja szybka wykorzystuje try
blok wokół wszystkiego między deklaracjami zmiennych a instrukcją return, z tylko catch{}
funkcją obsługi. Oczywiście wolna wersja jest taka sama, chyba że bez try / catch. Kod wywołujący (tj. Główny) jest taki sam w obu przypadkach i ma tę samą reprezentację zestawu (więc nie jest to kwestia kluczowa).
Zdemontowany kod dla szybkiej wersji:
[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
Zdemontowany kod dla wolnej wersji:
[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
W każdym przypadku *
pokazuje, gdzie debuger wszedł w prosty „krok”.
EDYCJA: OK, przejrzałem kod i myślę, że widzę, jak działa każda wersja ... i uważam, że wolniejsza wersja jest wolniejsza, ponieważ wykorzystuje mniej rejestrów i więcej miejsca na stosie. W przypadku małych wartości n
jest to prawdopodobnie szybsze - ale gdy pętla zajmuje większość czasu, jest wolniejsza.
Być może blok try / catch wymusza zapisywanie i przywracanie większej liczby rejestrów, więc JIT wykorzystuje je również w pętli ... co poprawia ogólną wydajność. Nie jest jasne, czy uzasadnione jest, aby JIT nie używał tylu rejestrów w „normalnym” kodzie.
EDYCJA: Właśnie wypróbowałem to na moim komputerze x64. CLR x64 jest znacznie szybszy (około 3-4 razy szybszy) niż CLR x86 w tym kodzie, a pod x64 blok try / catch nie robi zauważalnej różnicy.