JIT-оптимизации - Компиляция метода Method

ОГЛАВЛЕНИЕ

Когда JIT компилирует метод Method , он не имеет понятия о том, которое из двух реализаций должно быть вызвано – A.Foo или B.Foo. Поэтому практически невозможно встроить вызов в место встраивания. (Заметьте, что мы говорим "практически" – в теории возможно исполнить частичную оптимизацию, которая представит встраивание в отдельных случаях, но это мы обсудим потом, когда будем говорить об диспетчеризации метода интерфейса.)

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

В следующем примере код будет выработан на месте вызова (в методе Method):

007E0076  xor         esi,esi                   ; esi = 0 (i)
007E0078  jmp         007E00AA                  ; переход к условию сравнения
007E007A  mov         eax,esi                   ; i % 2 == 0?
007E007C  and         eax,80000001h
007E0081  jns         007E0088
007E0083  dec         eax  
007E0084  or          eax,0FFFFFFFEh
007E0087  inc         eax  
007E0088  test        eax,eax
007E008A  je          007E0098
007E008C  mov         ecx,2B3180h
007E0091  call        002A201C
007E0096  jmp         007E00A2
007E0098  mov         ecx,2B3100h
007E009D  call        002A201C
007E00A2  mov         ecx,eax                   ; ecx = a
007E00A4  mov         eax,dword ptr [ecx]       ; eax = a таблица методов
007E00A6  call        dword ptr [eax+38h]       ; вызов посредством смещения 0x38
007E00A9  inc         esi  
007E00AA  cmp         esi,0Ah
007E00AD  jl          007E007A
007E00AF  pop         esi  
007E00B0  ret

Как указывалось выше, виртуальный вызов диспетчеризуется посредством двух шагов: сначала входя в таблицу методов объекта (007E00A4 mov eax, dword ptr [ecx]) и затем вызывая указатель на метод в известном смещении в таблицу методов (007EE00A6 call dword ptr [eax+38h]).

Теоретически ключевое слово sealed C# (и его соответствующий напарник final из IL) предназначено для повышения эффективности в случае с диспетчеризацией виртуального метода посредством указания того, что хотя метод и виртуальный, он не может быть больше перегружен каким-либо порожденным классом. К примеру, следующий код не должен включать в себя диспетчеризацию виртуального метода, которую мы только что рассмотрели:

class A

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

{
    public override sealed void Foo() { }
}
class C : B
{
}
class Program
{
    static void Method(B b)
    {
    b.Foo();
    }
    static void Main(string[] args)
    {
        for (int i = 0; i < 10; ++i)
        {
            B b = (i % 2 == 0) ? new B() : new C();
            Method(b);
        }
     }
}

Ясно, что цель метода b.Foo в методе Method статически известна- будет вызван B.Foo. Несмотря на то, что  JIT не желает использовать эту информацию для предотвращения диспетчеризации виртуального метода, тем не менее, как мы можем убедиться из указанного ниже машинного кода, оно порождается (мы урезали установку объекта в данном блоке).

005000A2  mov         ecx,eax 
005000A4  mov         eax,dword ptr [ecx]
005000A6  call        dword ptr [eax+38h]

Заметьте, что использование ключевого слова sealed по отношению к классу не имеет никакого эффекта, даже притом, что цель вызова будет статически известна.

Чтобы полностью понять, что же происходит на заднем плане, вам стоит понять, что IL обладает двумя командами, используемыми для диспетчеризации вызовов методов: call и callvirt. Одной из основных причин использования команды callvirt для вызовов методов является то, что код, выработанный JIT, содержит проверку, которая обеспечивает отличие экземпляра от значения null, а в противном случае выдает исключение NullReferenceException. Вот почему компилятор C# вырабатывает команду callvirt IL даже при вызове не виртуальных методов экземпляров. Если бы все было не так, то следующий код мог бы успешно скомпилироваться и запуститься:

class A
{
    public void Foo() { }   // Foo не использует "this"
}
class Program
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 10; ++i)
        {
            A a = (i % 2 == 0) ? new A() : null;
            a.Foo();
        }
    }
}