Вокруг пакетов времени выполнения 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
выше. Надеюсь, Опции компилятора в IDE
Зная, что
$ImportedData Off
генерирует более оптимальный код доступа к глобальным переменным без отрицательных эффектов в автономном приложении, мы, конечно же, были бы весьма рады возможности включить её для всех модулей приложения разом. К сожалению, нет возможности просто сделать это. Как вы видите, страница Project | Options | Compiler Options не даёт возможности управления этой опцией... :(И поэтому вам нужно использовать одно из следующих решений:
- Вручную вставлять
{$IMPORTEDDATA OFF}
в начало каждого модуля проекта - Вручную вставить
{$I MyAppSettings.inc}
в начало каждого модуля проекта. В .inc файл вставить настройки проекта, в том числе -{$IMPORTEDDATA OFF}
- Скомпилировать проект вручную через вызов
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 можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.