JIT-оптимизации - Встраивание метода

ОГЛАВЛЕНИЕ

Встраивание метода

Короткий и простой метод, который используют многократно, может быть встроен в вызывающий код. На данный момент задокументировано (скорее, просто упоминается), что JIT может встраивать методы, которые короче, чем 32 бита, не содержать никакой сложной логики ответвлений, а также не иметь механизмов обработки исключений.

Изучив следующий фрагмент кода, давайте рассмотрим, что произойдет, когда JIT не оптимизирует и не встраивает вызовы метода, а также в случае, если это происходит.

public class Util
{
    public static int And(int first, int second)
    {
        return first & second;
    }
        [DllImport("kernel32")]
    public static extern void DebugBreak();
}
class Program
{
    private static volatile int _i;
    static void Main(string[] args)
    {
        Util.DebugBreak();
        _i = Util.And(5, 4);
    }
}

Заметьте, что используется функция kernel32.dll DebugBreak (которая в себе использует прерывание, int 3). Мы используем это для того, чтобы Windows предоставил возможность отладить процесс в то время, как будет вызван метод, тем самым нам не придется подсоединяться к нему вручную из Visual Studio или другого отладчика. Наконец, заметьте, что мы сделали поле _i свободным, тем самым его присваивание не будет оптимизировано.

При начале дизассемблирования с отключенными JIT-оптимизациями (к примеру, когда процесс начат в пределах отладочной сессии Visual Studio), будет выработан следующий код для вызова метода:

00000013  mov         edx,4 
00000018  mov         ecx,5
0000001d  call        dword ptr ds:[00A2967Ch]         ; это Util.And
00000023  mov         esi,eax
00000025  mov         dword ptr ds:[00A28A6Ch],esi     ; это  _i

Если мы продолжим вступать в метод в позиции 00A2967C, то мы найдем сам метод:

00000000  push        edi  
00000001  push        esi  
00000002  mov         edi,ecx
00000004  mov         esi,edx
00000006  cmp         dword ptr ds:[00A28864h],0
0000000d  je          00000014
0000000f  call        794F1116
00000014  mov         eax,edi
00000016  and         eax,esi
00000018  pop         esi  
00000019  pop         edi  
0000001a  ret

Заметьте, что здесь нет оптимизации или встраивания: параметры передаются в метод в регистрах EDX и ECX (конвенция быстрого вызова), и команда AND по смещению 0x16 выполняет назначение метода.

Давайте теперь изучим встроенный вызов, выработанный в то время, как мы подсоединяем процесс после срабатывания точки прерывания отладчика. Вот как выглядит вызов метода на этот раз:

00B90075  mov         dword ptr ds:[0A22FD0h],4    ; _i = 4
00B9007F  ret

Результатом AND(5, 4) будет 4 и это значение, которое напрямую записывается во временное поле.Заметьте, что оптимизация настолько агрессивна, что операция AND даже не выполняется – она не может быть выполнена напрямую во время компиляции.

Тем не менее, практически невозможно встроить вызов виртуального метода. Это, конечно же, происходит потому, что сам вызываемый метод неизвестен во время компиляции и может изменяться между вызовами метода. К примеру, представьте такую ситуацию:

class A
{
    public virtual void Foo() { }
}
class B : A
{
    public override void Foo() { }
}
class Program

{
    static void Method(A a)
    {
    a.Foo();
    }
    static void Main(string[] args)
    {
        for (int i = 0; i < 10; ++i)
        {
            A a = (i % 2 == 0) ? new A() : new B();
            Method(a);
        }
    }
}