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