JIT-оптимизации

ОГЛАВЛЕНИЕ

Компилятор .NET Just-In-Time Compiler (JIT) считается многими одним из основных преимуществ производительности CLR по сравнению с JVM и другими управляемыми средами, которые используют двоичный код, скомпилированный компилятором JIT. 

Данное преимущество делает свойства JIT столь секретными и JIT-оптимизации настолько мистическими. Это также является причиной того, почему SSCLI (Rotor) содержит всего лишь наивную реализацию быстрого JIT, которая производит только минимальные оптимизации при трансляции каждой команды IL в ее соответствующую последовательность команд машинного кода.

Далее последует краткий список JIT-оптимизаций, а за ним детальное пояснение диспетчеризации интерфейса метода и использования техник встраивания JIT.

До того, как мы начнем, следует упомянуть кое о чем. Во-первых, очень важно, что для того, чтобы увидеть JIT-оптимизации, вам стоит изучить машинный код, выработанный JIT-компилятором во время выполнения сборки вашего приложения. Тем не менее, есть небольшое предостережение: если вы решите изучить машинный код в среде отладчика Visual Studio, вы не увидите оптимизированный код. Это потому, что JIT-оптимизации по умолчанию отключены тогда, когда процесс запущен из отладчика (для удобства). Поэтому для того, чтобы увидеть JIT-оптимизированный код, вам стоит подсоединить Visual Studio к запущенному процессу, или запустить процесс из CorDbg, установив при этом флаг JitOptimizations (выполнив команду "mode JitOptimizations 1" из командной строки CorDbg). Наконец, поскольку у нас нет доступа к реальным исходникам JIT-компилятора, вам не стоит воспринимать данную информацию как нечто само собой разумеющееся.

Также стоит отметить то, что большая часть данной статьи основана на x86 JIT , который поставляется с .NET 2.0 CLR, хотя мы также изучим поведение x64 JIT.

Отключение проверки диапазона Range-check elimination

Так как при осуществлении доступа к массиву посредством цикла условие завершения цикла зависит от длины массива, то проверка на границы массива может быть удалена. Изучите следующий код. В порядке ли он?

private static int[] _array = new int[10];
private static volatile int _i;
static void Main(string[] args)
{
    for (int i = 0; i < _array.Length; ++i)
        _i += _array[i];
}

А вот как выглядит сгенерированный 32-битный код:

00BE00A5  xor         edx,edx                          ; edx = 0 (i)

00BE00A7  mov         eax,dword ptr ds:[02491EC4h]     ; eax = числа
00BE00AC  cmp         edx,dword ptr [eax+4]            ; edx >= длинна?
00BE00AF  jae         00BE00C2                         ; исключение!
00BE00B1  mov         eax,dword ptr [eax+edx*4+8]      ; eax = числа[i]
00BE00B5  add         dword ptr ds:[0A22FC8h],eax      ; _i += eax

00BE00BB  inc         edx                              ; ++edx (i)
00BE00BC  cmp         edx,0Ah                          ; edx < 100?
00BE00BF  jl          00BE00A7

Первая строка - вводная, строки посередине являются телом цикла, и три строки в конце представляют собой счетчик и тест на завершение цикла.

Третья строка в указанном коде (00BE00AC) производит проверку границ – она проверяет регистр EDX, используемый для индексации в пределах массива, на то, чтобы он не был более или равен длине массива [EAX + 4] (EAX содержит в себе адрес массива, который представляет собой длину массива в 4-битном смещении от старта). В дополнение, есть проверка на завершение цикла со второй и до последней строчки в листинге (00BE00BC).

А где же проверка на диапазон? Причиной такого поведения является тот простой факт, что ссылка на массив сама по себе статична в данном случае. Работа со статической ссылкой вызывает генерацию указанного выше кода (также обратите внимание, как в 00BE00A7 мы получаем ссылку на массив в регистр при каждой итерации цикла). Такое поведение может быть удалено простой модификацией кода:

private static int[] _array = new int[10];
private static volatile int _i;
static void Main(string[] args)
{
    int[] localRef = _array;
    for (int i = 0; i < localRef.Length; ++i)
    {
        _i += localRef[i];
    }
}

А вот как выглядит сгенерированный 32-битный код:

009D00D2  mov         ecx,dword ptr ds:[1C21EC4h]      ; ecx = числа
009D00D8  xor         edx,edx                          ; edx = 0 (i)
009D00DA  mov         esi,dword ptr [ecx+4]            ; esi = числа.длинна
009D00DD  test        esi,esi                          ; esi == 0?
009D00DF  jle         009D00F0
009D00E1  mov         eax,dword ptr [ecx+edx*4+8]      ; цикл
009D00E5  add         dword ptr ds:[922FC8h],eax       ; _i += числа[i]
009D00EB  inc         edx                              ; ++edx (i)
009D00EC  cmp         esi,edx                          ; edx > числа.длинна?
009D00EE  jg          009D00E1

Обратите внимание на отсутствие проверки границ, - теперь условие завершения цикла является единственной проверкой в данном коде.

Также стоит отметить, что очень легко сломать данную оптимизацию используя что либо другое вместо Array.Length (массив.длина) в качестве условия завершения цикла. К примеру, следующий код (с аналогичной функциональностью) сгенерирует проверку границ массива и испортит всю оптимизацию:

for (int i = 0; i < localRef.Length * 2; ++i)
{
    _i += localRef[i / 2];
}