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