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