JIT-оптимизации - Анализ потока
ОГЛАВЛЕНИЕ
Анализ потока
Анализ потока может определить, что конкретный статический тип используется для вызова метода интерфейса и поэтому может быть напрямую диспетчеризирован посредством типа вместо использования описанного выше метода.
IA a = new A();
a.Foo();
Практически ясно то, что A.Foo вызывается в данном месте, и неважно, что произойдет. Управляемый анализ потока компилятора может определить данное условие и выработать соответствующий двоичный код, который позволит JIT встраивать метод в случае, если он удовлетворяет другим требования для встраивания (смотрите выше).
Анализ частоты
Одна из реализаций интерфейса вызывается значительно чаще, чем другие, в определенном месте. Это может быть определено посредством динамического профилирования и исправления кода, или же какой либо подсказкой от программиста. В данном случае диспетчеризация метода интерфейса для данного конкретного интерфейса может быть изменена на прямую диспетчеризацию или даже встроенную, как это показано ниже:
if (obj->MethodTable == expectedMethodTable) {
// встроенный код метода "hot"
}
else {
// стандартный код размещения интерфейса метода
}
Подход JIT
В источниках не указывается, что JIT может выполнять какие либо оптимизации из данных двух. Тем не менее, поэкспериментировав с кодом, выработанным JIT, становится ясно, что все не так просто, как казалось.
Давайте рассмотрим место вызова в следующем коде и попробуем проанализировать, что произойдет в процессе диспетчеризации метода. Мы придали интерфейсу и методам пояснительные названия и значения, тем самым нам будет проще изучить данный пример .
interface IOperation
{
int Do(int a, int b);
}
class And : IOperation
{
public int Do(int a, int b) { return a & b; }
}
class Or : IOperation
{
public int Do(int a, int b) { return a | b; }
}
class Program
{
static volatile int _i;
static void Main()
{
for (int i = 0; i < 10; ++i)
{
IOperation op = (i % 2 == 0) ?
(IOperation)new And() : (IOperation)new Or();
_i = op.Do(5, 3);
}
}
}
В данном случае мы имеем одинокий вызов IOperation.Do, и JIT не может статически определить, какая реализация будет вызвана. Тем самым прямое встраивание или прямая диспетчеризация метода невозможна. Так каков код, который будет выработан в данном случае?
Давайте сначала изучим место вызова, что является точкой Main. Оно будет скомпилировано тогда, когда будет вызвана точка входа, поэтому мы можем сразу увидеть сам код. Далее идет код лишь для самого вызова IOperation.Do в цикле.
007E00A4 push 3
007E00A6 mov edx,5 ; установка параметров
007E00AB call dword ptr ds:[2C0010h] ; вызов
007E00B1 mov dword ptr ds:[002B2FE8h],eax ; сохранение возвращаемого значения
Заметьте, что это не шаблон диспетчеризации метода интерфейса, который мы должны здесь увидеть. Вместо этого мы получили непрямой вызов через адрес 002C0010. Вам стоит запомнить данный адрес, потому что мы будем еще упоминать его в дальнейшем описании. Пройдя дальше, мы увидим:
002C6012 push eax
002C6013 push 30000h
002C6018 jmp 79EE9E4F
Сама реализация не была еще скомпилирована и поэтому мы трасируем код JIT-компилятора. Со временем мы будем перенаправлены (посредством сложной цепи прыжков) к самому коду метода. Выполнение команды ret оттуда вернет нас назад в основной цикл (туда, где была выполнена команда call , 007E00AB).
Тем не менее, после того как цикл будет пройден 3 раза, код, на который указывает 002C0010 (вспомните, тут проходит наше исходное место вызова), будет возвращен в оптимизированную форму. Вот как она выглядит
002D7012 cmp dword ptr [ecx],2C3210h
002D7018 jne 002DA011
002D701E jmp 007E00D0
Помните, что заголовки объектов в .NET начинаются с таблицы методов объекта, и вот тривиальная оптимизация профилирования: если ожидается таблица методов (реализация типа "common" или "hot"), то мы можем выполнить переход напрямую к данному коду. (Заметьте, что реальный адрес встроен в команду, потому что JIT вернул код, добавив полную информацию о месторасположении метода. Это значит, что нет необходимости в лишнем доступе к памяти для диспетчеризации данного вызова. В противном случае нам придется пройти через стандартный уровень диспетчеризации, что мы вскоре и увидим. Для полноты, вот код в месте 007E00D0 (что является ожидаемым результатом команды CMP ):
007E00D0 and edx,dword ptr [esp+4]
007E00D4 mov eax,edx
007E00D6 ret 4
Это всего лишь реализация And.Do. Заметьте, что код, скомпилированный JIT не был встроен в место вызова или в помощника диспетчеризации. Тем не менее, прямой переход к данному коду должен вызывать как можно меньше затрат. Остается лишь вопрос о том, что находится по адресу 002DA011, или другими словами, что произойдет, если таблица методов будет не той, которую мы ожидали? На этот раз код гораздо сложнее.
002DA018 jl 002DA056
002DA01A push eax
002DA01B mov eax,dword ptr [ecx]
002DA01D push edx
002DA01E mov edx,eax
002DA020 shr eax,0Ch
002DA023 add eax,edx
002DA025 xor eax,3984h
002DA02A and eax,3FFCh
002DA02F mov eax,dword ptr [eax+151A6Ch]
002DA035 cmp edx,dword ptr [eax]
002DA037 jne 002DA04B
002DA039 cmp dword ptr [eax+4],30000h
002DA040 jne 002DA04B
002DA042 mov eax,dword ptr [eax+8]
002DA045 pop edx
002DA046 add esp,4
002DA049 jmp eax
002DA04B pop edx
002DA04C push 30000h
002DA051 jmp 79EED9A8
002DA056 call 79F02065
002DA05B jmp 002DA01A