среда, 3 августа 2011 г.

Хак №13: более быстрый доступ к глобальным данным ($ImportedData)

Это перевод Hack#13: Access globals faster ($ImportedData). Автор: Hallvard Vassbotn.

Вокруг пакетов времени выполнения Delphi (run-time packages) всегда была волшебная аура. Пакеты позволяют вам разделять Delphi код на более высоком уровне, чем это доступно с простыми DLL. Написание API библиотеки DLL включает в себя создание кучи плоских глобальных функций (анти-ООП) и избегание любых типов данных, хоть отдалённо сложнее Integer, Double, статических массивов, PChar и записей. Вы не можете обмениваться классами, объектами, глобальными переменными, не говоря уже про простые строки (если только и клиент и сервер не собраны в одной и той же версии Delphi и оба используют общий менеджер памяти).

Магия пакетов Delphi
А теперь войдём в мир магии пакетов Delphi. В нём вам не нужно беспокоиться и том, что вы можете, а что нет разделять между модулями - потому что теперь нет никаких ограничений! Пакеты являются способом разделения логического кода в физические модули для развёртывания. И это просто работает - как-то. "Как-то" - это (в большинстве) недокументированная магия.

Сегодня мы посмотрим только на одну часть этой недокументированной магии - как глобальные переменные могут разделяться между границами модулей (приложения и пакетов). А начнём мы с простейшего примера.

Предположим, что у нас есть модуль Unit1 с глобальной переменной GlobalVar: Integer и какой-то код в этом же модуле, её использующий. Вот примерный листинг такого кода:
Unit1.GlobalVar := 13;
004081B1 C705989240000D00 mov [GlobalVar],$0000000d
Это просто одна инструкция и здесь нет никакой косвенности, опосредованного доступа или указателей. Заметьте, что и адрес глобальной переменной и сама константа 13 (закодированная как 16-битный ShortInt $000D) включены непосредственно в машинную инструкцию. Когда я вычисляю адрес глобальной переменной через Ctrl + F7 (вычисляя @GlobalVar), я получаю $409298. Его вы также можете видеть в инструкции (в обращённом виде).

Теперь давайте посмотрим на магию пакетов. Если мы возьмём пакет PackageA, содержащий Unit1 с его GlobalVar: Integer, и DemoApplication, которое использует пакет PackageA и содержит код доступа к GlobalVar, то увидим, что компилятор на этот раз собирает такой код:
Unit1.GlobalVar := 42;
00403389 A1A4404000   mov eax,[$004040a4]
0040338E C7002A000000 mov [eax],$0000002a
Видите здесь косвенную связь? Глобальные переменные адресуются через указатель, который исправляется (fix up) при загрузке пакета, чтобы он указывал на нужную переменную. Это клёво и хорошо. Сама глобальная переменная находится внутри пакета, а приложение имеет волшебную "невидимую" глобальную переменную-указатель, которая указывает на настоящую переменную из пакета. Указатель получает значение при статической загрузке пакета (вероятно, его значение получается при помощи вызова GetProcAddress).

Глобальные переменные в автономных приложениях
Теперь давайте сделаем шаг назад и посмотрим на другую ситуацию. Сейчас мы посмотрели на случай использования переменной из того же модуля и из другого пакета - две крайние противоположные ситуации. Но что случится, если вы обратитесь к глобальной переменной через модуль (unit) в одном и том же модуле (module)? Ведь для этого не нужно косвенное обращение, не так ли? Давайте проверим.

Мы написали простое консольное приложение, состоящее из двух модулей (Unit1 и Unit2) и главной программы:
unit Unit1;

interface

var
  GlobalVar: integer = 42;

procedure SetGlobalLocally;

implementation

procedure SetGlobalLocally;
begin
  asm int 3 end; // остановимся здесь в отладчике 
  Unit1.GlobalVar := 13;
end;  

end.
unit Unit2;
  
interface

procedure SetGlobalIndirect;

implementation

uses Unit1;

procedure SetGlobalIndirect;
begin
  asm int 3 end; // остановимся здесь в отладчике
  Unit1.GlobalVar := 42;
end;  

end.
program TestImportedData;
{$APPTYPE CONSOLE}
uses
  Unit1 in 'Unit1.pas',
  Unit2 in 'Unit2.pas';
begin
  SetGlobalLocally;
  SetGlobalInDirect;
end.
Заметили эти милые жёстко зашитые точки останова? Точки останова отладчика реализуются перезаписью исходного машинного кода, записью поверх него команды программной точки останова (int 3), которая кодируется в код машинной инструкции $CC. Когда вы запускаете этот код под отладчиком, он остановится на этих двух инструкциях. Здорово, да? ;)

Первая точка останова находится в Unit1 непосредственно над кодом, модифицирующим глобальную переменную. В этом случае у нас происходит полностью локальный доступ и сгенерированный код будет тем, что мы видели выше - если вы запустите программу и посмотрите на сгенерированные инструкции в CPU-отладчике, то увидите, что никакого косвенного доступа тут нет:
Unit1.GlobalVar := 13;
004081B1 C705989240000D00 mov [GlobalVar],$0000000d
Вторая точка останова находится в Unit2, где мы изменяем глобальную переменную из "внешнего" модуля (из другого модуля, отличного от того, где были объявлена переменная). Заметьте, что мы всё ещё находимся внутри одного и того же .exe файла, так что здесь нет надобности в косвенном доступе, как это имеет место быть в случае с пакетами. Запустите код снова и, когда он остановится на второй точке останова, посмотрите в CPU-отладчик:
Unit1.GlobalVar := 42;
00403389 A1A4404000   mov eax,[$004040a4]
0040338E C7002A000000 mov [eax],$0000002a
Похоже, у нас есть косвенный доступ! Что происходит? Причиной для этой (небольшой) неэффективности является возможность включать и исключать модуль из пакета без перекомпиляции модуля (прим.пер.: т.е. .pas модуль компилируется один раз, получается .dcu, а затем этот .dcu может либо включаться в пакет, либо использовать в приложении напрямую. Один и тот же .dcu может быть как включён в состав пакета, так и не включён - а компилятор про это не знает в момент сборки модуля). Вот почему компилятор перестраховывается и генерирует код модуля для поддержки "худшего" случая: модуль будет экспортироваться из пакета. Важная вещь, которую нужно запомнить:
По умолчанию, весь кросс-модульный доступ к глобальным переменным происходит опосредованно, через указатель
Следствие: если у вас есть код, требовательный к производительности, то вам лучше бы закэшировать все глобальные переменные, если они расположены в других модулях. Но вы можете это сделать только если вам не нужно видеть изменения в этих переменных во время работы цикла (подумайте о случае многопоточного кода).

Для автономных приложений, которые вообще не используют пакеты - это несколько раздражает: почему мы должны страдать от не самого оптимального кода и генерируемых компилятором заглушек. Хотя размер кода и его производительность едва ли изменятся от убирания этой косвенной связи, эта возможность была бы приятным бонусом.

$ImportedData Off
И выходит, что у компилятора есть директива, которая делает именно это. Познакомьтесь: директива $ImportedData (aka $G). Вот что говорит про неё справка Delphi:
Тип: переключатель
Синтаксис: {$G+} или {$G-}
{$IMPORTEDDATA ON} или {$IMPORTEDDATA OFF}
По умолчанию: {$G+} {$IMPORTEDDATA ON}
Область действия: локальная
Примечания

Директива {$G-} отключает создание ссылок импортируемых данных. Режим {$G-} незначительно улучшает производительность, но блокирует модуль от включения в пакет.
По умолчанию действует режим {$ImportedData On} - это включает создание кода по опосредованному доступу к глобальным переменным из внешних модулей. Используя в модуле директиву {$ImportedData Off}, вы заставите компилятор не генерировать код-заглушку, приводя к несколько более оптимальному коду доступа к глобальным переменным. Заметьте, что вам нужно использовать директиву в каждом модуле, который использует глобальные переменные, а не просто в том модуле, где они (глобальные переменные) объявлены.

Давайте добавим третий модуль для тестирования этой возможности:
unit Unit3;
 
interface

procedure SetGlobalDirect;

implementation

uses Unit1;

{$IMPORTEDDATA OFF}

procedure SetGlobalDirect;
begin
  asm int 3 end; // остановимся здесь в отладчике
  Unit1.GlobalVar := 42; 
end;

end.
Этот код идентичен коду из модуля Unit2, но мы добавили директиву $ImportedData Off, чтобы указать компилятору на необходимость генерации оптимизированного кода доступа к глобальным данным. Запустите программу снова и когда она остановится на точке останова в Unit3 - посмотрите на CPU-отладчик:
Unit1.GlobalVar := 42; 
004033D1 C705A04040002A00 mov [GlobalVar],$0000002a
Ура! Мы избавились от перенаправления.
Директива компилятора {$ImportedData Off} заставляет компилятор генерировать более эффективный код доступа к глобальным данным.
Это работает отлично в Delphi 6 и 7. И, кажется, это больше не работает в Delphi 2006 (и, вероятно, в 2005). Похоже, в компиляторе появился баг. Директива просто игнорируется, а компилятор генерирует код с косвенным доступом, который мы видели для Unit2 выше. Надеюсь, Borland DevCo CodeGear Embarcadero исправят этот досадный баг в будущих версиях компилятора (прим.пер.: эта директива всё ещё не работает в Delphi XE).

Опции компилятора в IDE
Зная, что $ImportedData Off генерирует более оптимальный код доступа к глобальным переменным без отрицательных эффектов в автономном приложении, мы, конечно же, были бы весьма рады возможности включить её для всех модулей приложения разом. К сожалению, нет возможности просто сделать это. Как вы видите, страница Project | Options | Compiler Options не даёт возможности управления этой опцией... :(

И поэтому вам нужно использовать одно из следующих решений:
  1. Вручную вставлять {$IMPORTEDDATA OFF} в начало каждого модуля проекта
  2. Вручную вставить {$I MyAppSettings.inc} в начало каждого модуля проекта. В .inc файл вставить настройки проекта, в том числе - {$IMPORTEDDATA OFF}
  3. Скомпилировать проект вручную через вызов dcc32.exe, указав настройку в командной строке
Другими словами, включение этого режима на всё приложение целиком - достаточно неудобная операция.
Установить {$ImportedData Off} из IDE - тяжело
Я надеюсь, что разработчики Delphi сделают эту опцию доступной из настроек компилятора в IDE в будущих версиях среды. Фактически, я отправил это пожелание как запрос на улучшение в QC (пожалуйста, проголосуйте за него)! (прим.пер.: последние версии Delphi имеют в настройках проекта опцию "Additional options to pass to compiler", где вы можете ввести любые опции и переключатели - что, впрочем, никак не помогает в конкретно этом случае, потому что вышеуказанная директива игнорируется в Delphi XE).

Счёт: Delphi 7 против Delphi 2006: 1-1 ;)

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

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

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

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

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

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

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

Примечание. Отправлять комментарии могут только участники этого блога.