В комментарии этот ответ (который предлагает использовать операторы бит-сдвига над целым умножением/делением для производительности), я спросил, будет ли это на самом деле быстрее. В глубине моего сознания есть идея, что на каком-то уровне что-то будет достаточно умным, чтобы понять, что >> 1
и / 2
- это одна и та же операция. Однако теперь мне интересно, действительно ли это так, и если да, на каком уровне это происходит.
В тестовой программе создается следующий сравнительный CIL (с optimize
on) для двух методов, которые соответственно делят и сдвигают свой аргумент:
IL_0000: ldarg.0
IL_0001: ldc.i4.2
IL_0002: div
IL_0003: ret
} // end of method Program::Divider
против
IL_0000: ldarg.0
IL_0001: ldc.i4.1
IL_0002: shr
IL_0003: ret
} // end of method Program::Shifter
Поэтому компилятор С# испускает команды div
или shr
, не будучи умным. Теперь я хотел бы видеть фактический ассемблер x86, который производит JITter, но я понятия не имею, как это сделать. Возможно ли это?
изменить, чтобы добавить
Выводы
Спасибо за ответы, приняли тот из nobugz, потому что в нем содержится ключевая информация об этой опции отладчика. В конечном итоге для меня это было:
- Переключиться на конфигурацию Release
- В
Tools | Options | Debugger
отключите параметр "Подавлять оптимизацию JIT при загрузке модуля" (т.е. мы хотим разрешить оптимизацию JIT) - В том же месте выключите "Включить только мой код" (т.е. мы хотим отладить весь код)
- Поместите оператор
Debugger.Break()
где-нибудь - Сборка сборки
- Запустите .exe, а когда он сломается, отлаживается с использованием существующего экземпляра VS
- Теперь в окне "Разборка" отображается фактический x86, который будет выполнен
Результаты были просвечивающими, если не сказать больше - оказывается, JITter действительно может сделать арифметику! Здесь отредактированы образцы из окна "Разборка". Различные методы -Shifter
делятся на две степени с помощью >>
; различные методы -Divider
делятся на целые числа, используя /
Console.WriteLine(string.Format("
{0}
shift-divided by 2: {1}
divide-divided by 2: {2}",
60, TwoShifter(60), TwoDivider(60)));
00000026 mov dword ptr [edx+4],3Ch
...
0000003b mov dword ptr [edx+4],1Eh
...
00000057 mov dword ptr [esi+4],1Eh
Оба метода статически-разделить на 2 не только были встроены, но фактические вычисления были сделаны JITter
Console.WriteLine(string.Format("
{0}
divide-divided by 3: {1}",
60, ThreeDivider(60)));
00000085 mov dword ptr [esi+4],3Ch
...
000000a0 mov dword ptr [esi+4],14h
То же самое со статически-делением на 3.
Console.WriteLine(string.Format("
{0}
shift-divided by 4: {1}
divide-divided by 4 {2}",
60, FourShifter(60), FourDivider(60)));
000000ce mov dword ptr [esi+4],3Ch
...
000000e3 mov dword ptr [edx+4],0Fh
...
000000ff mov dword ptr [esi+4],0Fh
И статически-разделите-на-4.
Лучшее:
Console.WriteLine(string.Format("
{0}
n-divided by 2: {1}
n-divided by 3: {2}
n-divided by 4: {3}",
60, Divider(60, 2), Divider(60, 3), Divider(60, 4)));
0000013e mov dword ptr [esi+4],3Ch
...
0000015b mov dword ptr [esi+4],1Eh
...
0000017b mov dword ptr [esi+4],14h
...
0000019b mov dword ptr [edi+4],0Fh
Он встраивается, а затем вычисляет все эти статические деления!
Но что, если результат не статичен? Я добавил код для чтения целого числа из Консоли. Это то, что он производит для разделов на этом:
Console.WriteLine(string.Format("
{0}
shift-divided by 2: {1}
divide-divided by 2: {2}",
i, TwoShifter(i), TwoDivider(i)));
00000211 sar eax,1
...
00000230 sar eax,1
Итак, несмотря на то, что CIL отличается от других, JITTER знает, что деление на 2 сдвигается вправо на 1.
Console.WriteLine(string.Format("
{0}
divide-divided by 3: {1}", i, ThreeDivider(i)));
00000283 idiv eax, ecx
И он знает, что вам нужно разделить, чтобы разделить на 3.
Console.WriteLine(string.Format("
{0}
shift-divided by 4: {1}
divide-divided by 4 {2}",
i, FourShifter(i), FourDivider(i)));
000002c5 sar eax,2
...
000002ec sar eax,2
И он знает, что деление на 4 сдвигается вправо на 2.
Наконец (лучшее снова!)
Console.WriteLine(string.Format("
{0}
n-divided by 2: {1}
n-divided by 3: {2}
n-divided by 4: {3}",
i, Divider(i, 2), Divider(i, 3), Divider(i, 4)));
00000345 sar eax,1
...
00000370 idiv eax,ecx
...
00000395 sar esi,2
Он ввел метод и разработал лучший способ сделать что-то на основе статически доступных аргументов. Ницца.
Так что да, где-то в стеке между С# и x86, что-то достаточно умно, чтобы понять, что >> 1
и / 2
совпадают. И все это еще больше усугубило мое мнение о том, что добавление компилятора С#, JITter и CLR делает намного более умным, чем любые небольшие трюки, которые мы можем попробовать в качестве смиренных программистов приложений:)