COLLAPSE (C) by Vladimir Kladov, 2005

версия 4

Исходные данные. Delphi + KOL = уменьшение программ в 5-10 раз.

Задача: уменьшить еще больше, возможно за счет некоторого (незначительного) замедления.

Идея: встроить в KOL форт-подобную машину, часть кода переписать на Форт-подобном языке, с тем, чтобы откомпилированный в байт-код (П-код), он стал еще меньше. П-код будет исполняться чрезвычайно небольшим эмулятором П-машины (270 байт машинного кода), и позволит сжать код существенно больших по размеру процедур еще в 2 (как минимум) раза. Назовем эту идею "коллапс" (Collapse).

Требования к П-машине. Переход от выполнения обычного кода к выполнению П-кода и наоборот не должен быть слишком сложным. П-машина должна легко "вписываться" в среду Delphi-программы, работающей под управлением Windows. П-машина должна быть устроена так, чтобы из П-кода должно быть несложно вызвать и обычную процедуру или функцию, и API-функцию (т.е. в соглашении stdcall о передаче параметров и результатов), и метод объекта. Работа с полями объектов так же не должна быть сложной.

Архитектура П-машины. Форт-подобная машина - это стек-ориентированная машина, причем стек вычислений отделен от стека возвратов, что позволяет легко выносить в подпрограммы код, который встретился более чем однажды - если это дает сокращение кода. В П-машине нет такого понятия как регистры или аккумулятор - все вычисления выполняются на стеке, называемом "вычислительный". В нашем случае удобно вычислительный стек разместить на обычном машинном стеке. Для стека адресов возврата (там так же сохраняются некоторые служебные данные, не только адреса возврата) выделяется некоторая относительно небольшая область памяти (ее можно выделить динамически). Этот стек возвратов может (и нам удобно так сделать) расти вверх, а не вниз.

Содержимое кадра стека возвратов:

смещение,
обозначение
значение
0   RetAddr - адрес команды П-машины, к которой следует вернуться. Если = 0, то будет выполнен возврат к исполнению обычного кода (адрес возврата извлекаются из обычного стека, а регистры EBP, EBX, ESI, EDI - из стека возвратов, см. ниже).
4   SaveEBP - Сохраняет регистр EBP от вызвавшей процедуры (если это П-процедура, то EBP в ней использовался для хранения специального П-регистра SELF).

Таким образом, кадр возврата составляет 8 байт. Кроме того, когда П-процедура изначально вызывается из машинного кода, с текущей свободной позиции в стеке возвратов записываются следующие поля:

смещение значение
0 - регистр EDI
4 - регистр ESI
8 - регистр EBX
Далее следует описанный выше кадр возврата из П-процедуры, только вместо адреса возврата сохраняется 0
12 - 0 (вместо адреса возврата, 0 сигнализирует эмулятору П-кода, что надо вернуться в машинный код)
16 - регистр EBP

Команды П-машины. Все команды П-машины разбиваются на 2 группы:

Первая группа команд предназначена для выполнения ветвлений и короткой загрузки небольших по величине констант.

000x.xxxx - загрузка константы -16..+15 на вершину стека
001x.xxxx yyyy.yyyy - безусловный переход на -4096..+4095 байтов
010x.xxxx yyyy.yyyy - условный переход по нулю на стеке, на -4096..+4095 байтов
011x.xxxx yyyy.yyyy - условный переход по не нулю на стеке, на -4096..+4095 байтов

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

10xx.yyyy yyyy.yyyy - вызов до 4096 х 4 = 16384 подпрограмм по номеру в одной из четырех таблиц xx (xx=00 - таблица подпрограмм без параметров или параметрами на стеке, xx=01 - с одним параметром в EAX, xx=10 - с двумя параметрами в EAX и EDX, xx=11 - с тремя параметрами в EAX, EDX, ECX);
11xx.xxxx - короткий вызов до 64 первых подпрограмм из таблицы xx=00 (т.е. подпрограмм без параметров).

Данный набор команд обеспечивает возможность расширять систему команд П-машины новыми командами, реализуемыми как машинные подпрограммы. Никаких специальных команд не предусматривается, даже базовые операции, такие как возврат в вызвавшую процедуру (RETURN, EXIT) или вычислительные операции на стеке, или загрузка констант большей разрядности, чем числа -16..+15, реализуются подпрограммами.

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

Вызов эмулятора П-кода. Для эмуляции заданной П-процедуры эмулятор может быть вызван обычной командой вызова процедуры эмулятора (точка входа EmulatePCode), вслед за этим вызовом непосредственно располагаются коды команд П-машины. Такой вызов занимает 5 байт в случае использования машинной команды CALL. Есть еще одна возможность: использование специального обработчика исключений позволяет заменить такой 5-байтный CALL однобайтной машинной инструкцией. Использоваться может любая привилегированная или неверная машинная команда, в нашем случае выбор пал на команды IN и OUT с кодами E4..E7 (наличие ряда из 4х различных префиксов позволяют при вызове П-процедуры определить, сколько параметров из регистров EAX, EDX, ECX должно быть перемещено на вычислительный стек). Нестандартный обработчик исключений перехватывает возникающее исключение, обнаруживает, что оно вызвано привилегированной инструкцией с одним из этих кодов, модифицирует контекст исполнения так, как если бы была выполнена команда CALL EmulatePCode, уничтожает созданное исключение, и сообщает системе, что выполнение может быть продолжено. Такой подход позволяет сэкономить в коде программы k x 4 байт (где k = число П-процедур), хотя приводит к еще большему замедлению работы П-кода. Существенно исправляет эту ситуацию с замедлением следующий подход: в момент вызова машинной процедуры из П-процедуры, эмулятор П-кода сам проверяет, что машинная процедура начинается с одного из кодов E4..E7, и при наличии этого кода сам эмулирует вызов эмулятора, не приводя к вызову исключения в случае вызова из П-кода машинной процедуры, "замененной" П-кодом.

Такой подход позволяет переводить в П-код только указанные процедуры из машинного кода, в итоге заставляя П-код работать совместно с машинным кодом, и обеспечивать необходимый баланс между экономией кода и быстродействием.

Структура П-процедуры. П-процедура начинается с префиксного байта (E4..E7) и далее просто содержит набор П-команд, описанных выше. Обычно она завершается (возможно, в нескольких местах) командой EXIT или RETURN. П-процедура может получать до 3х параметров через регистры EAX, EDX, ECX, в соответствии с соглашением паскаля о передаче параметров, но в этом случае эти параметры будут получены в вычислительном стеке автоматически, количеством принимаемых через регистры параметров управляет префикс П-процедуры.

Распределение регистров эмулятора П-кода.
ESI всегда указывает адрес текущей (очередной) П-команды (т.е. код П-команды удобно может быть загружен машинной инструкцией LODSB).
EDI указывает на следующий кадр в стеке возвратов (т.е. [EDI-8] = адрес возврата в вызывавшую П-процедуру, или 0, если возврат требуется выполнить в машинный код).
EBP постоянно хранит некоторое значение, установленное в П-процедуре. Предполагается использовать его для хранения переменной Self, и команды загрузки этого регистра и выкладывания его значения на вершину соответственно называются SetSELF и LoadSELF.
EBX свободен для промежуточных вычислений, и может использоваться для хранения каких-либо значений (или адреса возврата) при вызове подпрограмм в машинных кодах, в том числе API-функций (поскольку API-функции, так же как и процедуры Паскаля не портят регистры ESI, EDI, EBP и EBX). Так же, после возврата в П-процедуру из вызванной функции (не только машинной), возвращающей результат в регистре EAX, этот результат копируется в регистр EBX, и может быть помещен на вершину стека командой RESULT (если результат требуется). Прочие регистры (EAX, EDX, ECX) задействуются для временных операций в процессе извлечения, распознавания и выполнения П-кода.

Встраивание эмулятора П-кода в приложение. Практически, весь эмулятор - это небольшая процедура (чуть больше 260 байт) EmulatePCode. Способы вызова ее описаны пунктом выше. Работа этой процедуры сводится к тривиальному циклу выборки П-команд (указатель текущей П-команды - в регистре ESI), и их выполнения. Выполнение команд не требует большого числа операций, и сводится в основном к выполнению условных и безусловных переходов, и к выполнению подпрограмм, адреса которых выбираются из таблиц. Таблицы представляет собой массивы адресов подпрограмм, которые могут быть вызваны из любой П-процедуры (массивы эти объявлены в коде как глобальные процедуры в ассемблерном коде CollapseProcTable, CollapseProcTable1, CollapseProcTable2, CollapseProcTable3).

Эффективность вынесения общих участков кода в П-подпрограмму. Вынесению общих участков П-кода весьма способствует то, что вычислительный стек отделен от стека возвратов. Кроме того, благодаря краткости команд П-машины, вынесение общего участка не добавляет слишком много дополнительных байтов на оформление вынесенного кода, так что вынесение общего кода с точки зрения экономии размера оказывается эффективным даже в случае достаточно небольших участков кода. А именно, при вынесения в отдельную П-процедуру требуется увеличить код на 2 байта: префикс (E4..E7) и байт завершения (00 или 01, код команды EXIT или RETURN). Кроме того, для ссылки на П-процедуру потребуется указатель на нее в общей таблице = 4 байта. Итого, 6 байтов. Если этот участок кода используется в вызывающей ее процедуре дважды, то экономия составит n x 2 - 6 - 2 x L байт, при размере вынесенного участке n байт и при длине команды вызова L. Т.е. для двукратного использования в той же процедуре и 1-байтной команде вызова, участок должен быть не меньше чем 5 байт (экономия = 2 байта). Подсчитаем, например, сколько можно сэкономить байтов, если выносится участок кода длиной 12 байтов, и используется он в 2х П-процедурах, а для вызова используется 2х-байтная форма команды вызова. Формула эффективности дает: 12 х 2 - 6 - 4 = 14 байтов экономии, что уже даже больше, чем размер самого выносимого участка кода.

Компиляция П-кода. Для того, чтобы не кодировать П-процедуру в машинном коде вручную, в дистрибутив входит компилятор П-кода. Когда процедура (например, на Паскале) переписывается в П-код, она может в этом виде располагаться внутри комментария, поскольку ее текст все равно не будет распознан компилятором Delphi корректно. Физически новый вариант может размещаться вместе с прежним (паскалевским или ассемблерным) кодом. Этот исходный П-код процедуры должен быть П-компилятором преобразован в код на языке встроенного в Delphi ассемблера, фактически - в набор операторов DD, DB и прочих ассемблерных конструкций, который Delphi уже будет в состоянии понять и превратить в байты. (Почему не записать просто байты? Это не всегда возможно: в теле П-процедуры могут быть адреса, смещения, вычисляемые как разница адресов, или символические константы, адрес или значение которых могут становиться известны только в момент окончательной компиляции Delphi-программы). Для того, чтобы компилятор мог успешно выполнить свою работу по модификации исходного текста, он должен суметь определить местонахождение исходного П-кода, а так же место, куда должен быть вставлен результирующий ассемблерный код, включая случай, когда после модификации исходного кода он должен убрать результат предыдущей компиляции, чтобы заместить его новым оттранслированным П-кодом. Соответственно, предлагается следующая конструкция:

{$IFDEF Pcode}
  ... здесь программист разместит правильный заголовок процедуры ...
  {$IFDEF Psource}
    ... здесь размещается исходный П-код ...
  {$ENDIF Psource}
  ... сюда П-компилятор вставляет результат своей работы ... (ассемблерный код)
{$ELSE OldCode}
... здесь остается прежний вариант процедуры, с таким же заголовком ...
{$ENDIF OldCode}

Ветка OldCode вовсе необязательна, П-компилятор ориентируется по сигнальным строкам {$IFDEF Psource} и {$ENDIF Psource}. Соответственно, символ Pcode должен быть определен (его можно не определять, чтобы оттранслировать программу в прежнем варианте, без использования Collapse, но тогда паскалевский или ассемблерный - машинный - вариант кода должен присутствовать в ветке OldCode), а символ Psource - никогда не должен быть определен, иначе это вызовет ошибки компиляции.

Например:

{$IFDEF Pcode}
  function Max2( x, y: integer ): integer;
  {$IFDEF Psource}
  PROC(2)
    DUP C2 - >0?
    IF1 BEGIN SWAP ENDIF DEL
    EXIT

  ENDP
  {$ENDIF Psource}
  asm
    DB $E6
    DB $C0, $C1, $C2
    DB $20, $01
    DB $C3
    DB $C4
    DB $C5
  end;

{$ELSE OldCode}
  function Max2( x, y: integer ): integer;
  begin
    if x < y then Result := y else Result := x;
  end;

{$ENDIF OldCode}

(данный пример не самый удачный, машинный код процедуры Max2 при включенной оптимизации в Delphi в итоге занимает те же 8 байт, что и получившаяся П-процедура. В реальности, такие короткие процедуры таким способом укорачивать не стоит, внимание следует уделить большим процедурам).

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

Директива начала процедуры. Располагается в отдельной строке, имеет вид:
[name[:]] PROC[(n)] [INLINE|SMART[ FUNC[TION]]]
, где n - число от 0 до 3. PROC(0) эквивалентно PROC. Число n задает число параметров из регистров EAX, EDX, ECX, которые при вызове П-процедуры из машинного кода помещаются на вычислительный (обычный) стек в обратном порядке - так, как если бы процедура имела соглашение stdcall о передаче параметров. (На самом деле, никто не мешает при переводе паскалевской процедуры, функции или метода в П-код заодно объявить ее теперь с директивой stdcall, и тогда такая "пперегонка" параметров во время исполнения не потребуется. Более того, если все процедуры, переведенные в П-код, при переводе преобразованы к соглашению stdcall, становится возможным еще и сэкономить, и использовать более эффективный код для передачи управления между процедурами. Но это требует переделки эмулятора, и внесения изменений в существующий рабочий код - KOL.pas, например, а там уже накоплен довольно большой объем ассемблерного кода, ориентированного на передачу параметров именно через регистры).
     Директива INLINE означает, что объявляемая процедура при вызове ее из П-кода будет "подставляться", а не "вызываться" (всегда - для макросов с параметрами в имени, если процедура используется из П-кода и создание отдельного кода процедуры не является более выгодным с точки зрения экономии кода). Предназначена она в основном для библиотечных П-процедур, и прежде всего - для процедур-макросов, которые отдельную процедуру не формируют, а в зависимости от переданных параметров, вызывают те или иные П-процедуры.

Директива завершения процедуры. ENDP - так же должна располагаться в отдельной строке. Это просто директива компилятору - "прекратить компиляцию процедуры"., после чего может быть до финального маркера {$ENDIF Psource} размещено еще несколько процедур, на этот раз обязательно именованных. Это будут процедуры, в которые выносятся подпрограммы из компилируемой в код исходной процедуры. Фактически они являются локальными подпрограммами, и встраиваются компилятором П-кода в тот же самый блок asm .. end - для некоторой опртимизации размера кода (известно, что Delphi "округляет" размер создаваемой процедуры до 4х байт, выравнивая начало каждой на границу двойного слова).

Основной синтаксис операторов, разделители. Единицей П-языка является "слово", отделенное от соседних пробелами. Никаких специальных правил для переноса на другую строку нет, просто каждое слово вместе с возможными параметрами в скобках(в том числе когда скобок несколько) должно записываться на одной строке. Но в одной строке возможно записать сколько угодно слов, в том числе меток, разделенных пробелами. Различие в регистре написания не имеет значения (раз уж П-язык встраивается в среду Delphi-Pascal, не различающую регистр в именах, то удобно так же поступить и в П-языке, чтобы не создавать лишнюю путаницу. Таким образом, SWAP, Swap, swap, sWaP - это всегда одно и то же имя).

Комментарии. Все, что начинается с символов //, считается комментарием до конца строки.
Все, что начинается с символов
(*, считается комментарием до символов *) (в том числе, когда строк несколько). Фигурные скобки для комментариев не используются, и зарезервированы для целей отладки (в них размещаются конструкции П-кода, которые компилируются только при включенной опции "отладка"). Исключением является конструкция {$ }, которая переносится в результирующий asm-код без изменений. Прежде всего, это нужно для директивы {$R }, но может пригодиться и для других директив (в том числе, вообще говоря, директив условной компиляции - только это может приводить к некорректному подсчету смещений и к генерации неверного асм-кода).

Метка. Отличается от всех прочих слов тем, что завершается символом ':'. Например, русская/метка: - это допустимая метка, 1: - так же допустимая метка, +-%!: так же допустимая метка!

Безусловный переход. GOTO(метка) , метка здесь записывается без "своего" двоеточия.

Условные переходы. IF0(метка) , IF1(метка) , и т.д. Внутри "слова", напоминаю, никаких пробелов не допускается, но внутри скобок пробелы могут быть.

Условные конструкции. Для упрощения написания кода, вводятся такие конструкции:

IF0 операторы ENDIF  и IF0 операторы ELSE операторы2 ENDIF-  эквивалентно: IF1(L1) операторы1 GOTO(L2) L1: операторы2 L2:

IF1 операторы ENDIF  и IF1 операторы1 ELSE операторы2 ENDIF  -  эквивалентно: IF0(L1) операторы1 GOTO(L2) L1: операторы2 L2:

Конструкции условной компиляции. Мало отличаются от обычных условных конструкций и имеют форму: IF(выражение) операторы ENDIF или IF(выражение) операторы1 ELSE операторы2 ENDIF или даже IF(выражение1) ... ELSEIF(выражение2) ... ELSEIF(выражение3) ... ELSE ... ENDIF. Конструкция эта прежде всего предназначена для использования в П-макросах с параметрами (см. далее). Выражение в скобках при IF обязано быть константным с точки зрения П-компилятора (а не компилятора Delphi). Разница в том, что именованные константы Delphi для П-компилятора не являются константами. Но имена параметров могут участвовать в выражении наравне с константами.

Вставка числовой константы. Может использоваться для того, чтобы сформировать непосредственный операнд для "команды", или даже описать саму "команду", код которой точно известен (так описаны в библиотеке П-команд встроенные команды с жестко заданным кодом). Для задания одного байта используется префикс #, для двухбайтной константы - префикс ##, для трех-байтной, соответственно, ###, и для четырехбайтной, префикс ####. Далее следует сама константа, которая может представлять собой выражение, которое будет вставлено в код между скобками asm..end как есть, без изменений (если выражение требует для своей записи пробелов, оно должно быть заключено в скобки). Например: #1 ##1000 ###$ABCDEF ####MyTable . Иногда запись в такой форме несколько неудобна, и тогда лучше использовать одну из (эквивалентных) конструкций B(байт) , W(слово) , T(трехбайтное) , D(двойное слово) , Q(8-байтное) .

Вставка строковой константы. Строка в кавычках, записанная по правилам паскаля, вставляет все свои символы как байты, непосредственно в код. Никакой завершающий ноль не вставляется при этом, если он требуется, его следует записать дополнительно: 'строка символов' #0 . Такой способ размещения строк может быть достаточно удобным для использования их как непосредственные константы в коде. К сожалению, автоматическое вынесение совпадающих строк за скобки не предполагается, и при повторной вставке той же самой строки, она будет дублироваться в коде. В случае необходимости избежать дублирования строк рекомендуется использовать (глобальные) именованные константы, и ссылаться на них по именам.

Вызовы процедур. Все прочие конструкции предназначены для вызова подпрограмм. Независимо от того, о П-процедурах идет речь, или о процедурах самого Delphi, простое "называние" имени процедуры приводит к тому, что обеспечивается наличие соответствующей ссылки в таблице локальных процедур (если только указанная процедура не включена уже в системную таблицу процедур CollapseSysProcTable), и вставке байта с кодом, который будет рассматриваться эмулятором П-кода как обращение к этой процедуре через таблицу. Например, если мы вызываем ShowMessage , достаточно просто "употребить", или "назвать" это имя процедуры в коде. Такой же бесхитростный способ вызывает и П-процедуры из базовой библиотеки П-процедур (которая располагается в файле с именем baselib.pas). В первую очередь компилятор П-кода пытается обнаружить процедуру в списке локальных П-процедур, размещающихся непосредственно вслед за данной П-процедурой, затем - в базовой библиотеке baselib.pas, и если ее не находит, то далее (не проверяя) предполагает, что речь идет об имени процедуры, которое в данном модуле доступно компилятору Delphi, и формирует ссылку на нее в локальной таблице процедур, и вставляет соответствующую команду в код.

Процедуры с параметрами (макросы с параметрами). Если в имя процедуры входят круглые скобки, то содержимое скобок рассматривается как макро-параметр. Для каждого параметра должны быть свои собственные скобки. Например, Proc(param1)(param2)want_to(param3) - можно рассматривать как одно "слово", фактически - имя процедуры. Среди доступных (сначала локальных, затем - в базовой библиотеке) П-процедур указанная процедура будет отыскиваться по шаблону имени Proc(*)(*)want_to(*). Далее параметры подставляются, и полученная таким образом процедура компилируется как обычно. В теле такого макроса возможно использовать описанные выше конструкции условной компиляции IF(выражение)...ENDIF, и переданные параметры могут участвовать в этом выражении. Например, напишем макрос L(число), который грузит на вершину вычислительного стека заданное число, или константу, известную П-компилятору, максимально эффективным (т.е. по возможности более коротким) кодом:

L(N) PROC INLINE
IF(N >= -16 AND N <= 15)             #(N)
ELSEIF(N >= 0 AND N <= 255)          LoadByte #N
ELSEIF(N >= -128 AND N <= 127)       LoadSignedByte #(N AND 255)
ELSEIF(N >= 256 AND N <= 65535)      LoadWord ##N
ELSEIF(N >= -32768 AND N <= 32767)   LoadSmallInt ##(N AND $FFFF)
ELSEIF(N >= $10000 AND N <= $FFFFFF) Load3 ###N
ELSEIF(N >= 0 AND N <= $7FFFFFFF)    Load4 D(N)
ELSEIF(N < 0 AND -N <= $7FFFFFFF)    Load4 D(N)
ELSE                                 Load8 Q(N)
ENDIF
     ENDP L(N)

Если процедура с параметрами не является INLINE-процедурой, то для каждого возможного параметра должен был бы создан отдельный экземпляр этой процедуры. Т.к. Deplhi не понимает имена со скобками (и вообще, имена для процедур могут содержать недопустимые символы), П-компилятор должен присвоить этой процедуре некоторый уникальный идентификатор и далее ссылаться на него, когда формирует код. Тогда правило следующее: исходный П-код такого макроса размещается в части implementation, в скобках

{$IFDEF Pmacro}
  {$IFDEF Psource}
    исходный П-код
  {$ENDIF Psource}
{$ENDIF Pmacro}

А в части interface оставляется место для размещения П-компилятором своих заголовков (заголовки формируются вида procedure Pxxxxx; - для каждого сгенерированного макроса).

Например:

{$IFDEF Pmacro}
{$IFDEF Psource}
C(N) PROC
     L(N) Copy EXIT
     ENDP

{$ENDIF Psource}

  здесь для каждого N, для которого будет вызван макрос C(N), будет сформирована собственная точка входа, например:
//by PCompiler: С(1)
  procedure P000001;

  asm
  DB $F4, 0, 9, $80+idxCopy, 0
  end;

{$ENDIF Pmacro}

(примечание: idxCopy здесь - индекс процедуры Copy в системной таблице процедур - это только в случае, если она там есть, иначе указатель на Copy будет добавлен в локальную таблицу и ссылка будет индексом по локальной таблице. В коде, сгенерированном П-компилятором, будет просто число).

Комментарий // C(1) должен добавить сам П-компилятор, чтобы "знать", какому набору значений параметров соответствует имя P000001. В том месте, где будет выполнен вызов C(1), П-компилятор должен будет подставить ссылку на P000001;

Вообще, это сложно для реализации, поэтому в упрощенной версии П-компилятора он не будет поддерживать такие макросы, и будет требовать наличия атрибута INLINE для всех макросов. Соответственно, для макросов C(N) должны быть выписаны варианты не-INLINE-процедур, уже не макросы: C1, C2, C3, C4, ... (пока не надоест).

Макросы без параметров. Используются в основном для того, чтобы упростить запись выражений, и позволить использовать специальные символы (например, знаки операций) вместо того, чтобы каждый раз использовать имена соответствующих функций. Например, знак + просто определяется как макрос, вызывающий библиотечную процедуру Add. Имеются следующие макросы:

знак библиотечная функция стек до стек после; примечания
+ Add X Y X+Y
- Sub X Y X-Y
(-) Neg X Y X -Y
* Mul X Y lo(X*Y)
** MulMul X Y lo(X*Y) hi(X*Y)
/ IntDiv X Y (X div Y)
/: DivMod X Y (X div Y) (X mod Y)
:= Store X Y ; //[Y]=X
^ IntPow X Y X*X*X...X; X^Y
>< Swap X Y Y X
<< ShiftL X Y (X shl Y)
>> ShiftR X Y (X shr Y)

С учетом этих сокращений, функция для расчета корней квадратного уравнения может быть записана следующим образом:

SquareRoot PROC                         // A, B, C -> X1, X2, n
  C(1) DUP *      
                     // A, B, C, B*B
  C(3) L(4) *     
                     // A, B, C, B*B, 4*A
  C(2) *          
                     // A, B, C, B*B, 4*A*C
  -               
                     // A, B, C, B*B-4*A*C
  DUP <0? IF1 L(4) DELN L(0) EXIT ENDIF
// 0
  ISQRT           
                     // A, B, C, d
  >< DEL          
                     // A, B, d
  DUP IF0 DEL >< / L(2) / L(1) ENDIF
   // -B/(2*A), 1
  DUP C(2)                             
// A, B, d, B, d
  + (-) C(3) / L(2) /
                  // A, B, d, x1
  Swp(2)          
                     // A, x1, d, B
  - >< Swp(1)     
                     // x1, d-B, A
  / L(2) / L(2) EXIT  
                  // x1, x2, 2

  ENDP

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

Когда INLINE или SMART процедура компилируется вставкой, ассемблерный код для нее все равно генерируется - на тот случай, когда процедура так же вызывается не только из П-кода, но и из обычного машинного кода. Более того, это позволяет после компиляции и сборки П-компилятору обнаружить этот факт, и учесть это обстоятельство (а именно, отказаться от вставки кода в вызывающую процедуру, если код все равно уже существует, и его вызов короче, чем код самой процедуры, что верно в большинстве случаев).

Отличие в синтаксисе INLINE и SMART-процедур заключается в том, что они не должны завершаться командой EXIT или RETURN, и при формировании кода для вызова к ним добавляется команда EXIT. Это означает что П-процедуры INLINE и SMART не могут быть функциями. Как вариант, возможно зарезервировать команды RESULT, RESULTB, RESULTW, RESULTSMALLINT (и вообще все команды, начинающиеся с RESULT), и считать, что если имеется такая команда непосредственно вслед за вызовом вставляемой INLINE-функции, то это слово не следует компилировать, а при генерации кода для внешнего вызова будет использоваться не код EXIT, а RETURN. Это не дает 100%-гарантии корректности работы такой функции одновременно как INLINE-вставки и как откомпилированного в байт-код П-кода, вызываемого из машинного кода, поэтому возможно расширить синтаксис словом FUNC[tion], для INLINE и SMART-функций, чтобы П-компилятор точно знал, что в случае самостоятельной компиляции   такой функции надо завершать ее именно RETURN.

Вызов паскаль-процедур из П-кода . В принципе возможно выполнить особый вызов процедур, имеющих соглашение о передаче параметров через регистры EAX, EDX, ECX. В П-машине эти регистры недоступны, поэтому сначала параметры помещаются на вершину вычислительного (обычного) стека обычным способом, а затем выполняется вызов процедуры с соответствующим префиксом. Префикс - это байт, фактически "команда", которая выталкивает из стека нужное количество параметров, записывая их в регистры EAX, EDX, ECX, а затем берет следующий за самим префиксом байт и рассматривает его как обычный код вызова подпрограммы, эмулируя вызов машинной подпрограммы. Достаточно префиксов PASCAL1, PASCAL2 и PASCAL3 - соответственно, для случаев одного параметра в EAX, двух в EAX и EDX, и трех параметров в EAX, EDX и ECX. Этот способ нехорош, т.к. он требует относительно много кода и замедляет эмуляцию многочисленными перебросками данных из стека на регистры и из регистов на стек.

Если вызываемую процедуру преобразовать к соглашению stdcall, то для ее вызова из П-кода будет происходить быстрее. Тем более, что в случае, когда сама эта процедура преобразована в П-код, отказ от использования префикса еще и существенно ускорит выполнение, так как при таком вызове не будет тогда использоваться механизм исключений.

Укороченный вызов подпрограмм. В обычном машинном коде команды вызова подпрограммы занимает 5 байт. В П-коде, если адрес вызываемой процедуры заносится в таблицу процедур, и она вызывается единожды, расходуется так же ровно 5 байт (в случае использования 1-байтного вызова), или 6 байт, в большинстве случаев. Для функций следует добавить длину команды RESULT, обычно + 1 байт. Экономия начинается только со случая, когда процедура вызывается более одного раза.

Для процедур, имеющих фиксированный адрес в адресном пространстве задаче, практически всегда выполняется условие, что адрес этот обычно не слишком отличается от базы адресного пространства (значение по умолчанию $400000). Даже если использовать для записи такого адреса 3 байта, этого оказывается достаточно. Соответственно, если бы Delphi мог воспринимать 3х-байтные константы, то можно было бы сэкономить в случае, если использовать специальный вариант вызова - с 3-х байтным непосредственным операндом. Решение об использовании такой конструкции можно было бы оставить на волю П-компилятора (он бы генерировал 4-хбайтный код для вызова подпрограммы, если она не перечислена в системной таблице процедур, и вызывается из данной П-процедуры однократно, это могло бы дать экономию 1 байт на каждом таком вызове). К сожалению, Delphi отказывается работать во встроенном ассемблере с адресами процедур иначе как через директиву DD. Так что придется принять все как уже оно задумано, и экономить по возможности теми способами, которые приемлет Delphi.

Существует еще одна возможность экономии. В случае, когда локальная процедура отстоит от вызывающей ее процедуры не более чем на 256 байт (а при использовании П-кода в силу его компактности это вполне возможно), может быть использован специальный 2х-байтный вызов локальной подпрограммы. Решение об использовании 2х-байтного варианта вызова можно возложить на компилятор П-кода, т.к. количество байт до локальной П-процедуры ему известно. Если такое решение принимается, адрес локальной процедуры не добавляется в локальную таблицу процедур (экономия 4 байт), а потребление вычисляется как (число вызовов) х 2. Кроме того, при использовании такого способа всегда можно полагать, что вызывается П-код, и тогда нет необходимости в префиксном байте F4 - экономия еще 1 байта, и эта П-процедура всегда наследует локальную таблицу родительской П-подпрограммы, т.е. тогда экономится еще 1 байт KN. Т.е. в случае 3х-кратного вызова таким способом затрачивается 6 байта (-1 байт префикса, итого 5), а в случае обычного 3-х кратного вызова через локальную таблицу 7 байт. Т.е. такой способ перестает экономить размер кода, если число вызовов превышает 4.

Отладочные конструкции. Служебный макрос ASSERT(n), где n- числовое выражение, может использоваться для контроля указателя стека по сравнению со значением, запоминаемым в стеке возвратов. В случае, если разница между текущим ESP и сохраненным в кадре возврата не совпадает с n, генерируется ошибка. Чтобы макрос ASSERT генерировал код только в случае, когда включен режим компиляции "для отладки", его следует заключать в {фигурные скобки}. Специальный вызов DEBUG LINE 'строка' #0 позволяет разместить номер строки и имя модуля в начало П-процедуры. Он должен размещаться обязательно самым первым в П-процедуре, и необходимо заключать его так же в фигурные скобки, чтобы он компилировался только в отладочном режиме. Когда срабатывает ASSERT, имеется обычно достаточно данных, чтобы определить начало проблемной процедуры (в случае, если она имеет локальную таблицу процедур, ее адрес находится в регистре EBP, соответственно байт [EBP-1] содержит количество входов в таблицу, и начало процедуры тогда несложно вычисляется. После чего определение наличия кода команды DEBUG в этой позиции и отображение последующей строки - совсем не сложно. Для П-процедуры, которая не имеет собственной таблицы локальных подпрограмм, такой вариант был бы невозможен. Поэтому, в отладочном режиме локальная таблица путем добавления одного пустого элемента создается для всех П-процедур, кроме локальных, явно образованных путем вынесения общих участков кода. Для последних, срабатывание ASSERT будет приводить к выводу информации о родительской П-процедуре, что тоже, в общем-то, неплохо.

Кроме того, само по себе использование операторов в фигурных скобках может быть использовано в целях отладки: для формирования лог-файлов, для дополнительных проверок диапазонов данных, и т.д. Поскольку этот код компилируется только при включенном режиме отладки, на финальный вариант программы он не оказывает никакого влияния.

Еще одна отладочная конструкция предназначена только для этапа компиляции: ERROR('строка'). Если дело дошло до попытки откомпилировать данный П-оператор, текст строки выдается в качестве сообщения об ошибке, и компиляция файла завершается с ошибкой. С помощью директивы ERROR, в сочетании с операторами условной компиляции, возможно, например, контролировать корректность входных параметров макроса.

Кандидаты на коллапс. В среде разработки Delphi+KOL+MCK, первоочередными кандидатами на использование техники коллапса в целях экономии кода являются процедуры инициализации формы, автоматически генерируемые MCK. ( примечание: MCK - Mirror Classes Kit - является генератором кода, который работает под управлением Delphi на стадии разработки - так называемое время design-time. KOL - Key Objects Library - является библиотекой времени исполнения и содержит исходный код объектов, генерация первоначального создания и настройки свойств которых и является задачей MCK. Более подробно см. документацию и статьи на моем сайте http://bonanzas.rinet.ru ). В отличие от Delphi VCL, свойства формы и ее дочерних объектов хранятся не в ресурсах, из которых VCL извлекает их в процессе генерации формы в момент ее создания, а настраиваются кодом, который генерирует MCK в   design-time. Не слишком сложная задача по генерации соответствующего П-кода так же может быть добавлена (уже добавлена - для основного набора компонентов) в MCK. Компилятор П-кода для генерации ассемблерной версии П-кода, воспринимаемой Delphi, так же может быть вызван из MCK (это уже реализовано, но не всегда срабатывает, поэтому возможность ручного вызова П-компилятора так же оставлена).
    Данный подход может существенно снизить размер кода, затраченный на создание формы, ввиду повышенной компактности П-кода по сравнению с обычным машинным кодом (тем более, что MCK не генерирует ассемблерные конструкции, а то, что получается в результате работы компилятора Delphi, не всегда получается слишком компактным).

Еще один первейший кандидат - это функции KOL, конструирующие контролы, и возможно, обработчики сообщений (второе - опционально, т.к. может оказаться недопустимо большим замедление работы). Нестандартная отрисовка контролов (BitBtn, отрисовка графических контролов) так же - возможный кандидат. Дальнейшие кандидаты - это все, что по размеру превышает 40 байт кода :-)

Компилятор П-кода. Такой компилятор ( консольное приложение PCompiler.exe ) возможно запустить из командной строки, указав в качестве параметра один или несколько .pas- и .inc- файлов, или целую директорию (тогда будут просмотрены на предмет П-компиляции все .pas и .inc-файлы в этой директории).
Дополнительные параметры:
/Bbaselibpath - путь к директории, в которой следует брать файл baselib.pas, или точное имя файла, который следует использовать вместо baselib.pas.
/Pproctable - путь к директории, содержащей CollapseProcTable.inc или прямое указание имени такого файла (как указывалось выше, файл этот содержит список до 64К процедур, на которые имеются ссылки в системной таблице процедур). Весь проект обязательно должен быть перекомпилирован (сначала - П-компилятором, потом Delphi), если содержимое этого списка изменилось!
/D - включает режим "отладка".
/Mprojectname.map - указывает имя map-файла, который используется П-компилятором после первой компиляции для выявления использованных в коде процедур (и правильного формирования таблицы процедур), этот же мап-файл используется отладчиком для соотнесения адресов памяти и процедур.
/Ccommand
- команда компиляции проекта (вызов DCC32.exe со всеми требуемыми параметрами). При наличии этой опции, после первой компиляции файлов проекта, П-компилятор самостоятельно выполяет эту команду, выполняет анализ сформированного map-файла, и далее может пересортировать таблицу процедур и выполнить компиляцию повторно, если таблица изменилась.
Если в качестве параметра указана директория, и в этой директории имеется файл PCcompiler.cfg, то опции компиляции сначала берутся из него, а уже затем применяются дополнительные параметры, если они имеют место (т.е. параметры в самой командной строке все равно имеют приоритет). Формат файла PCcompiler.cfg следующий: это одна или несколько строк, содержащих все те же опции /B, /S, /D. Все строки, начинающиеся с символа '#', считаются комментариями.

Работа П-компилятора с файлами. П-компилятор не предназначен для распознавания синтаксиса Паскаль-кода. Его работу можно рассматривать как работу препроцессора, который отыскивает "свой" код между строго фиксированными директивами $IFDEF ... $ENDIF, и в строго заданном месте вставляет результат своей работы. Следовательно, П-компилятор не может всерьез заниматься разрешением межмодульных зависимостей. По это причине только один файл - baselib.pas может считаться для П-кода источником глобальных П-макросов, все прочие макросы должны быть определены в том же файле, что П-код, использующий эти П-макросы. Правило видимости для макроса - от места его определения и до конца файла (т.е. он макрос всегда должен быть определен выше, чем используется).

В первую очередь всегда "компилируется" файл baselib.pas (или тот, что задан в качестве его "заместителя" - в командной строке или конфигурационном файле). Заодно коллекционируются макроопределения, имеющиеся в этом файле (далее они могут использоваться во всех прочих файлах). Для каждого входного файла, включая baselib.pas (а этот файл в любом случае подлежит компиляции), П-компилятор находит все определения П-процедур, формирует ассемблерный код, размещает его вслед за определение П-процедуры (возможно, замещая результат своей предыдущей компиляции), и сохраняет новое содержимое файла (но только в случае, если файл действительно изменился в результате компиляции). На этом вся работа П-компилятора и завершается.

Точнее, завершается этап компиляции, и П-компилятор вызывает Delphi для перекомпиляции проекта, после чего анализирует сформированный map-файл, вычисляет, какие П-процедуры реально включены в код проекта, подсчитывает частоты использования вызываемых из П-процедур подпрограмм, пересортировывает таблицу процедур, и если она изменилась - начинает П-компиляцию всего проекта заново. Но на этот раз достаточно еще раз вызвать Delphi-компилятор, и завершить работу уже без анализа map-файла. Итого, П-компилятор можно считать 4-х -проходным.

Средства отладки. Как уже упоминалось выше, операторы П-кода, заключенные в фигурные скобки }, компилируются только в случае, если в опции П-компилятора включена опция /D. Дополнительно вводится ключевое слово Line, которое компилируется в двухбайтное число, содержащее номер текущей компилируемой строки. Библиотечная процедура Debug принимает последующие параметры: Line и строку, завершающуюся нулевым байтом, загружает адрес этой последовательности в регистр EBP (т.е. загружается адрес номера строки, за которым следует имя исходного файла).

П-компилятор при выполнении компиляции сохраняет дополнительную информацию о смещении каждой компилируемой команды относительно первой команды каждой П-процедуры - в отдельном файле. Таким образом, отладчик имеет доступ к информации о текущем месте исполнения.

Отладчик - это приложение в виде dll (Pdebuger.dll), которое подключается к отлаживаемому проекту из процедуры EmulatePCode и вызывается перед исполнением каждой П-команды. Отладчик позволяет выполнять отлаживаемый П-код по шагам (но только П-код, машинные процедуры он отлаживать не может, для этого должны использоваться штатные средства Delphi). Он сможет (в будущем) так же ставить точки останова, позволяет просматривать ячейки памяти, стек, стек возвратов, видеть исполняемый код в его исходном коде, и вообще говоря, этого достаточно для пошаговой отладки кода.

Для того, чтобы исполнимый код мог присоединить отладочную dll, он должен знать о том, где она располагается. Можно либо скопировать эту dll в директорию проекта, либо в файле Pcompiler.cfg указать путь к директории отладчика вместе с ключом /D[путь на PDebuger.dll].


Последнее изменение: 5.12.2005

http://bonanzas.rinet.ru

e-mailt: StrReplace( 'bonanzas#online.sinor.ru', '#', '@' )