То, как вы рассчитываете время, выглядит для меня довольно неприятно. Было бы гораздо разумнее просто рассчитать время всего цикла:
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)]
вместо этого не помогло ...
В принципе, вам нужно взглянуть на оптимизированный код JITted в cordbg, я подозреваю ...
РЕДАКТИРОВАТЬ: еще несколько бит информации:
- Размещение try / catch вокруг только
n++;
строки по-прежнему улучшает производительность, но не настолько, как размещение всего блока
- Если вы поймете конкретное исключение (
ArgumentException
в моих тестах), это все еще быстро
- Если вы печатаете исключение в блоке catch, оно все равно быстро
- Если вы перебрасываете исключение в блоке catch, оно снова медленно
- Если вы используете блок finally вместо блока catch, он снова замедляется
- Если вы используете блок finally, а также блок catch, это быстро
Weird ...
РЕДАКТИРОВАТЬ: Хорошо, у нас есть разборка ...
Для этого используется компилятор 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. CLR x64 намного быстрее (примерно в 3-4 раза быстрее) CLR x86 в этом коде, а в x64 блок try / catch не имеет заметного различия.