В предыдущей статье мы рассмотрели как компилятор реализует вызовы не виртуальных и виртуальных методов. Мы также обсудили обоснование и семантику динамических методов. Как вы помните, динамические методы работают так же, как и виртуальные методы, только медленнее. В этой статье мы зароемся в магию компилятора и поддержку 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" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.