Есть ли способ увидеть собственный код, созданный JITter для данного С#/CIL?

В комментарии этот ответ (который предлагает использовать операторы бит-сдвига над целым умножением/делением для производительности), я спросил, будет ли это на самом деле быстрее. В глубине моего сознания есть идея, что на каком-то уровне что-то будет достаточно умным, чтобы понять, что >> 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 делает намного более умным, чем любые небольшие трюки, которые мы можем попробовать в качестве смиренных программистов приложений:)

Ответ 1

Вы не получите значимых результатов, пока не настроите отладчик. Инструменты + Опции, Отладка, Общие, отключить "Запретить оптимизацию JIT при загрузке модуля". Переключитесь в конфигурацию режима выпуска. Пример фрагмента:

static void Main(string[] args) {
  int value = 4;
  int result = divideby2(value);
}

Вы делаете это правильно, если разборка выглядит так:

00000000  ret  

Вам придется обмануть оптимизатор JIT, чтобы заставить выражение оцениваться. Использование Console.WriteLine(переменная) может помочь. Тогда вы должны увидеть что-то вроде этого:

0000000a  mov         edx,2 
0000000f  mov         eax,dword ptr [ecx] 
00000011  call        dword ptr [eax+000000BCh] 

Yup, он оценил результат во время компиляции. Хорошо работает, не так ли.

Ответ 2

Да. Visual Studio имеет встроенный дизассемблер для этого. Однако вам нужно добавить команду в свою панель меню. Перейдите в раздел "Дополнительно" / "Настроить/Команды" (я не знаю, действительно ли они так называются в английской версии) и добавьте команду "Разборки", которая является отладочной отладкой, где-то в строке меню.

Затем установите контрольную точку в своей программе и, когда она сломается, щелкните эту команду Disassembly. VS покажет вам дизассемблированный машинный код.

Пример вывода для метода Divider:

public static int Divider(int intArg)
    {
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  push        ebx  
00000006  sub         esp,34h 
00000009  mov         esi,ecx 
0000000b  lea         edi,[ebp-38h] 
0000000e  mov         ecx,0Bh 
00000013  xor         eax,eax 
00000015  rep stos    dword ptr es:[edi] 
00000017  mov         ecx,esi 
00000019  xor         eax,eax 
0000001b  mov         dword ptr [ebp-1Ch],eax 
0000001e  mov         dword ptr [ebp-3Ch],ecx 
00000021  cmp         dword ptr ds:[00469240h],0 
00000028  je          0000002F 
0000002a  call        6BA09D91 
0000002f  xor         edx,edx 
00000031  mov         dword ptr [ebp-40h],edx 
00000034  nop              
    return intArg / 2;
00000035  mov         eax,dword ptr [ebp-3Ch] 
00000038  sar         eax,1 
0000003a  jns         0000003F 
0000003c  adc         eax,0 
0000003f  mov         dword ptr [ebp-40h],eax 
00000042  nop              
00000043  jmp         00000045 
    }

Ответ 3

Пока вы отлаживаете (и только во время отладки), просто нажмите на Debug - Windows - Разборка или нажмите соответствующую комбинацию Ctrl + Alt + D.