воскресенье, 17 июля 2011 г.

Реализация динамических методов компилятором

Это перевод Dynamic methods compiler implementation. Автор: Hallvard Vassbotn.

В предыдущей статье мы рассмотрели как компилятор реализует вызовы не виртуальных и виртуальных методов. Мы также обсудили обоснование и семантику динамических методов. Как вы помните, динамические методы работают так же, как и виртуальные методы, только медленнее. В этой статье мы зароемся в магию компилятора и поддержку RTL, которые используются для поддержки динамических методов. Отметим, что большая часть механики, используемой для динамических методов, используется также для методов-сообщений (message methods) - с той лишь разницей, что методы-сообщений позволяют программисту указывать индекс метода (он же - номер сообщения, положительное 16-разрядное число).

Вызов динамического метода

Хотя не-виртуальный метод кодирует адрес целевого кода непосредственно в инструкции процессора, а виртуальный метод использует опосредованный адрес в VMT с использованием фиксированного смещения - вызов динамического метода весьма отличается от них. Все вызовы динамических методов всегда вызывают один и тот же целевой код - волшебную подпрограмму RTL в модуле System, называемую _CallDynaInst. Эта процедура имеет два параметра: указатель на экземпляр (в EAX) и 16-битный SMALLINT селектор (в SI).

К примеру:
type
  TMyClass = class
    procedure FirstDynamic; dynamic;
    procedure SecondDynamic; dynamic; 
  end;
// …
var
  Instance: TMyClass;
begin
  Instance := TMyDescendent.Create;
  Instance.FirstDynamic;
  Instance.SecondDynamic;
end.
Это генерирует такой код для двух вызовов динамических методов:
TestDmt.dpr.334: Instance.FirstDynamic;
004096D6 8BC3             mov eax,ebx
004096D8 66BEFFFF         mov si,$ffff
004096DC E8E7A2FFFF       call @CallDynaInst
TestDmt.dpr.335: Instance.SecondDynamic;
004096E1 8BC3             mov eax,ebx
004096E3 66BEFEFF         mov si,$fffe
004096E7 E8DCA2FFFF       call @CallDynaInst
Обратите внимание на две различные константы, загружаемые в регистр SI (младшее слово регистра ESI): $FFFF и $FFFE. Как вы можете видеть, это шестнадцатеричные представления SMALLINT значений -1 и -2, соответственно. Так что эффект от вызова различных динамических методов заключается в передаче различных числовых констант в волшебную вспомогательную процедуру _CallDynaInst. Во время компиляции компилятор присваивает уникальное отрицательное число каждому динамическому методу в классе - это значит, что вы можете иметь не более 32768 динамических методов в классе - я думаю, что это более чем достаточно для большинства случаев!

Когда компилятор присваивает числовые значения для динамических методов класса, он также заполняет таблицу динамических методов (Dynamic Method Table - DMT), связывая значение (aka. селектор или индекс DMT) с адресом метода. Подпрограмма _CallDynaInst проверяет эту таблицу во время выполнения, пытаясь найти соответствие для индекса DMT, которое было передано ей в регистре SI. Если это удаётся, она делает переход (JMP) по найденному целевому адресу. Если нет - то она переходит к сканированию DMT родительского класса. Если в итоге соответствия не будет найдено - это вызовет run-time ошибку 210 (которую SysUtils превращает в исключение EAbstractError).

Вызов динамического метода из BASM

Если вы окажетесь в (маловероятной?) ситуации, когда вам необходимо вызвать динамический метод из ассемблера, то вы можете использовать относительно новую директиву DMTIndex для получения индекса динамического метода для конкретного метода. Давайте сначала просто получим этот индекс:
function MyDynamicMethodIndex: integer;
asm
  MOV EAX, DMTIndex TMyClass.FirstDynamic
end;

procedure Test;
begin
  Writeln(MyDynamicMethodIndex);
end;
При условии, что у нас есть определение TMyClass из фрагмента кода выше, этот пример выведет число -1. Очень полезно и интересно, правда? :-P Давайте сделаем ещё ​​один шаг вперёд и на самом деле вызовем метод из ассемблерного кода:
procedure CallFirstDynamicMethod(Self: TMyClass);
asm
  MOV ESI, DMTIndex TMyClass.FirstDynamic;
  CALL System.@CallDynaInst
end;
Таким образом, вызов из BASM динамического метода заключается в загрузке индекса в ESI использованием директивы DMTIndex с полным именем класса и метода, и последующем вызове волшебной подпрограммы System.@CallDynaInst (компилятор проецирует префикс _ волшебных функций RTL в символ @, что делает невозможным их прямой вызов из кода на Pascal-е). Обратите внимание, что _CallDynaInst (и его друг _CallDynaClass) использует нетрадиционное соглашение вызова: параметры передаются в EAX и E(SI) - причина в том, что он не может использовать регистры, ведь динамический метод сам по себе может использовать для передачи параметров (ECX и EDX). И во всех случаях EAX содержит указатель Self.

Заметьте, что BASM также поддерживает вызов динамических методов статически, без полиморфного диспетчеризирования:
procedure StaticCallFirstDynamicMethod(Self: TMyClass);
asm
  CALL TMyClass.FirstDynamic // Статический вызов
end;
Но обычно это не то, чего вы хотите.

Ускорение вызовов динамических методов

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

Если экземпляр остаётся неизменным на протяжении всего цикла, можно использовать переменную типа procedure of object.
procedure SlowDynamicLoop(Instance: TMyClass);
var
  i: integer;
begin
   for i := 0 to 1000000 do
     Instance.FirstDynamic;
end;

procedure FasterDynamicLoop(Instance: TMyClass);
var
  i: integer;
  FirstDynamic: procedure of object;
begin
  FirstDynamic := Instance.FirstDynamic;
   for i := 0 to 1000000 do
     FirstDynamic;
end;
Здесь мы оптимизировали цикл путём перемещения поиска динамического метода вне цикла. Если алгоритм проходит через список различных экземпляров, и вы можете гарантировать, что список является однородным (содержит экземпляры одного класса - прим.пер.), то можно использовать процедурную переменную и явно передать указатель Self, например:
procedure SlowDynamicListLoop(Instances: TList);
var
  i: integer;
  Instance: TMyClass;
begin
  for i := 0 to Instances.Count-1 do
  begin
    Instance := Instances.List[i];
    Instance.FirstDynamic;
  end;
end;

procedure FasterDynamicListLoop(Instances: TList);
var
  i: integer;
  Instance: TMyClass;
  FirstDynamic: procedure(Self: TObject);
begin
  FirstDynamic := @TMyClass.FirstDynamic;
  for i := 0 to Instances.Count-1 do
  begin
    Instance := Instances.List[i];
    Assert(TObject(Instance) TMyClass);
    FirstDynamic(Instance);
  end;
end;
В режиме assert мы проверяем, что наше предположение справедливо. На самом деле, такая оптимизация будет работать даже в тех случаях, когда у вас есть гетерогенный список объектов TMyClass (т.е. список из экземпляров TMyClass и дочерних к нему классов - прим.пер.) - пока все подклассы не переопределяют динамический метод. Мы можем проверить это так:
function TMyClassFirstDynamicNotOverridden(Instance: TMyClass): boolean;
var
  FirstDynamic: procedure of object;
begin
  FirstDynamic := Instance.FirstDynamic;
  Result := TMethod(FirstDynamic).Code = @TMyClass.FirstDynamic;
end;

procedure FasterDynamicListLoop2(Instances: TList);
type
  PMethod = TMethod;
var
  i: integer;
  Instance: TMyClass;
  FirstDynamic: procedure (Self: TObject);
begin
  FirstDynamic := @TMyClass.FirstDynamic;
  for i := 0 to Instances.Count-1 do
  begin
    Instance := Instances.List[i];
    Assert(TObject(Instance) is TMyClass);
    Assert(TMyClassFirstDynamicNotOverridden(Instance));
    FirstDynamic(Instance);
  end;
end;
Заметьте, что на практике от этих оптимизаций пользы не много. Хорошо разработанное программное обеспечение, вероятно, изначально не использует динамические методы, и уж конечно ему не следует использовать динамические методы в критичных по времени операциях. Тем не менее, в редких случаях, когда необходимо вызвать динамический метод третьей стороны в цикле, вы теперь знаете, как можно оптимизировать такие циклы.

В следующей статье я копну ещё глубже, обнажая структуру DMT.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.