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];
}