Автор:
Бред Родригес
Дата публикации: 1993
оригинал статьи взят
MOVING FORTH
Перевел:
mOleg
Портирование Форта. часть 2.
Изучение производительности.
Теперь посмотрим, какой ответ на каждый вопрос проектирования даст кодирование.
Понятно, вы не хотите писать одно ядро различными методами только для оценки различных методик. К счастью вы можете получить очень хорошее «ощущение» на очень малом наборе слов Форт-ядра.
Гай Келли [KEL92] рассматривал следующие 19 примитивов для 19 различных Фортов на IBM-PC.
NEXT … «адресный интерпретатор» который связывает слова в определении. Он используется в конце каждого низкоуровневого определения, и является одним из наиболее важных факторов, определяющих скорость работы Форт-системы. Вы уже видели псевдокод для него в косвенном, прямом шитом кодах (в подпрограммном коде его роль исполняет пара машинных команд CALL и RETURN).
ENTER ... так же называемый DOCOL или DOCOLON; действие поля кода, позволяющее исполняться двоеточному высокоуровневому определению. Это тоже критично для скорости исполнения, так как используется в начале каждого двоеточного определения. Не нужно для подпрограммного ШК.
EXIT ... называемый ;S в fig-Forth; код, завершающий исполнение высокоуровневого определения. Это, по сути, высокоуровневый возврат из подпрограммы, и он встречается в каждом двоеточном определении. В подпрограммном ШК это лишь машинная команда RET.
NEXT, ENTER, и EXIT определяют производительность механизма адресного интерпретатора. Они должны быть закодированы для оценки эффективности работы косвенного, прямого и подпрограммного шитых кодов. Они так же отображают эффективность вашего распределения регистров IP, W, и RSP для используемого процессора.
DOVAR ...так же известный как "variable" - фрагмент машинного кода, который определяет работу поля кода для всех переменных в Форте.
DOCON ...так же исзвестный как "constant" - фрагмент машинного кода, определяющий работу поля кода для всех констант.
DOCON и DOVAR вместе с ENTER показывают эффективность работы с полем параметров исполнимого слова. Это отражает удачность вашего выбора регистра W. В системах с прямым ШК так же показывает что выгоднее использовать в поле кода JUMP или CALL.
LIT ... или «literal»; это Форт слово извлекает значение, хранимое в ячейке памяти, находящейся сразу за LIT. Различные слова используют такие данные в коде, и это слово хорошо показывает их эффективность. Это так же показывает удачность выбора регистра IP.
@ … оператор извлечения данных, показывает на сколько быстро ведется работа с памятью системы из выскоуровневых определений. Это слово обычно показывает на сколько выгодно реализована работа с TOS.
! ... оператор сохранения данных в памяти, другой индикатор эффективности работы с памятью. Он потребляет два значения со стека, и показывает эффективность работы со стеком данных. Это хороший индикатор для выбора местоположения виртуального регистра TOS (в памяти либо в регистре процессора).
+ ... оператор сложения, это представитель от всей арифметики и логических операций. Подобно слову ! он показывает на сколько эффективна работа со стеком и точная демонстрация выгоды от размещения TOS в регистре процессора.
Это прекрасный выбор фрагментов шитого кода. От себя добавлю несколько своих любимцев:
DODOES ... действие поля кода слов, построенных с помощью конструкции CREATE DOES> . Этот код не сильно влияет на скорость работы системы, но может показать эффективность расположения виртуальных регистров: W, IP, RSP. Я его добавляю потому что это самый необычный код в ядре Форт-системы. Если вы можете, набросайте код DODOES . Запутанность DODOES будет развеяна в следующей части статьи.
SWAP ... простой стековый оператор, но тем не менее учебный.
OVER ... более сложный стековый оператор. Показывает вам, на сколько легко вы можете работать со стеком данных.
ROT ... еще более сложный стековый оператор, и скорее всего вам потребуется дополнительный временный регистр для его реализации. Если же вам удастся обойтись без временного регистра, вам скорее всего вообще не нужен регистр X .
0= ... один из немногих однопараметрических арифметических операторов, и скорее всего показывает выгодность выбора регистра для TOS.
+! ... наиболее иллюстративный оператор, комбинирующий работу стека, арифметики, чтения и записи памяти. Это один из любимых бенчмарков, хотя он реже используется, нежели другие перечисленные слова.
Перечисленные слова наиболее часто используются в Форт-ядре. Оптимизация их работы себя оправдывает. Я покажу примеры для всех перечисленных слов, включая псевдокод для процессора 6809, для остальных процессоров я буду использовать только избранные примеры для отражения специфических особенностей реализации.
Первый вариант: Motorola 6809
В мире 8-и битных процессоров 6809 – мечта Форт программистов. Он поддерживает два стека! Он так же имеет два других адресных регистра, и богатство ортогональных режимов адресации («ортогональный» означает, работающий одинаково и имеющий одинаковые опции для всех адресных регистров). Два 8-и битных аккумулятора могут рассматриваться как один 16-битный аккумулятор, так же имеется много 16-битных операций.
Программная модель 6809 следующая [MOT83]:
A – 8 битный аккумулятор
B – 8 битный аккумулятор
Большинство арифметических операций используют аккумулятор как приемник. А и В могут быть объединены и рассматриваться как один 16битный аккумулятор D (A- старший разряд, а В – младший).
X - 16 битный индексный регистр
Y - 16 битный индексный регистр
S - 16 битный указатель стека
U - 16 битный указатель стека
Все адресные режимы для X и Y так же могут использоваться с регистрами S и U.
PC - 16 битный счетчик команд
CC - 8 битный регистр условий
DP - 8 битный страничный регистр
Режим прямой адресации в 6800 ветке процессоров использует 8-битный адрес для работы с нулевой страницей памяти. 6809 позволяет прямо адресовать любую страницу памяти, регистр DP хранит старшие 8 бит адреса.
Два регистра указателя стека просятся для Форта. Они похожи, за исключением того, что S используется для подпрограммных вызовов и прерываний. Давайте будем последовательны, и будем использовать S для хранения адресов возвратов, оставив U для стека данных.
W и IP оба должны быть адресными регистрами, таким образом, логично использовать Х и Y для этих целей. Х и Y эквивалентны, поэтому давайте своевольно договоримся: X=W и Y = IP.
Теперь должна быть выбрана модель адресной интерпретации. Я набросаю прямой и косвенный ШК чтобы получить традиционный Форт. Лимитирующий быстродействие фактор – подпрограмма NEXT. Давайте посмотрим на оба его варианта в прямом и косвенном ШК:
<pre>
ITC-NEXT: LDX ,Y++ (8) (IP)->W, увеличить IP
JMP [,X] (6) (W)->temp, перейти на адрес, хранимый в temp
DTC-NEXT: JMP [,Y++] (9) (IP)->temp, увеличить IP,
Выполнить переход на адрес, хранимый в temp ("temp" внутри 6809)
</pre>
NEXT на 6809 в прямом ШК реализуется с помощью единственной машинной команды! Это означает, что вы можете кодировать его в виде двухбайтовых вставок маш.кода, что одновременно и быстрее и меньше, чем JMP NEXT. Для сравнения посмотрите на логику работы NEXT для подпрограммного ШК:
<pre>
RTS (5) ... в конце низкоуровневого слова
JSR nextword (8) ... в цепочке
... ... начало следующего низкоуровневого слова
</pre>
В подпрограммном ШК переход на следующее слово в определении занимает 13 тактов, по сравнению с 9-ю тактами в прямом ШК. Это потому что подпрограммный ШК постоянно сохраняет и восстанавливает адреса возвратов на стеке, в то время как простой прямой или косвенный ШК в пределах одного определения этого не делают.
Сделав выбор в пользу прямого ШК, вы должны решиться, должен ли вызываться высокоуровневый код с помощью машинной команды CALL или JUMP в его поле кода.
Направляющее размышление – как быстро вы можете получить адрес поля параметров, следующего за кодом. Давайте посмотрим на код для входа в двоеточное определение, используя символические имена виртуальных Форт-регистов:
С использованием JSR (Call):
<pre>
JSR ENTER (8)
...
ENTER: PULS W (7) получить адрес следующий за JSR в регистр W
PSHS IP (7) сохранить старое значение IP на стеке возвратов
TFR W,IP (6) адрес поля параметров => IP
NEXT (9) ассемблерное макро для JMP [,Y++]
37 всего циклов
</pre>
С использованием JMP:
<pre>
JMP ENTER (4)
...
ENTER: PSHS IP (7) сохранить старый IP на вершину стека возвратов
LDX -2,IP (6) повторно извлечь адрес поля кода
LEAY 3,X (5) добавить 3 и положить в регистр IP (Y)
NEXT (9)
31 циклов всего
(в скобках отражается количество циклов процессора на исполнение команды)
</pre>
NEXT на 6809 процессоре в прямом ШК не требует существования регистра W: имеются режимы косвенной адресации. Версия ENTER с использованием JMP должна повторно перечитать адрес поля кода, так как NEXT не оставляет его ни в одном из регистров, и затем добавить 3, чтобы получить адрес поля параметров. JSR версия может получать адрес поля параметров напрямую за счет извлечения адреса с вершины стека возвратов. Даже при этих условиях JMP версия быстрее. (Пример для студентов: попытайтесь написать JSR ENTER при условии S=PSP и U=RSP.)
В любом случае код слова EXIT одинаковый:
<pre>
EXIT: PULS IP вытолкнуть старое значение IP со стека возвратов
NEXT продолжить адресную интерпретацию
</pre>
Осталось зафиксировать несколько регистров. Вы можете хранить указатель на пользовательскую область (UP) в памяти, и этот Форт будет оставаться достаточно быстр.
Но тогда останется неиспользованный регистр “DP”, который больше не на что использовать. Давайте, используем описанный выше трюк, и будем хранить старший байт UP в регистре “DP”. Подразумевается, что младший байт регистра UP сброшен в нуль.
Остается один 16-битный регистр: “D” Большинство арифметических операций работают с ним. Стоит ли его использовать как временный регистр, либо кэшировать в нем TOS? 6809 команды используют память как операнд, таким образом второй рабочий регистр не обязателен. И если временный регистр необходим, легко сохранять “D” в стеке. Давайте напишем тестовые примитивы для обоих вариантов и посмотрим какой быстрее.
NEXT, ENTER, и EXIT не используют стек данных, и поэтому выглядят одинаково в любом случае.
DOVAR, DOCON, LIT, и OVER требуют одинаковое количество циклов процессора в любом случае. Следующее иллюстрирует, что хранение TOS в регистре часто только меняет место, где встретится команда PUSH или POP :
<pre>
TOS в D TOS в памяти псевдокод
DOVAR: PSHU TOS LDD -2,IP адрес поля кода => D
LDD -2,IP ADDD #3 адрес поля параметров => D
ADDD #3 PSHU D поместить D на вершину стека
NEXT NEXT
DOCON: PSHU TOS LDX -2,IP адрес поля кода => W
LDX -2,IP LDD 3,X содержимое поля параметров => D
LDD 3,X PSHU D вытолкнуть D в стек даных
NEXT NEXT
LIT: PSHU TOS LDD ,IP++ (IP) => D, увеличить IP
LDD ,IP++ PSHU D вытолкнуть D в стек даных
NEXT NEXT
OVER: PSHU D LDD 2,PSP второй элемент стека данных => D
LDD 2,PSP PSHU D вытолкнуть D в стек даных
NEXT NEXT
</pre>
SWAP, ROT, 0=, @, и особенно + всегда быстрее, если TOS находится в регистре:
<pre>
TOS в D TOS в памяти псевдокод
SWAP: LDX ,PSP (5) LDD ,PSP (5) TOS => D
STD ,PSP (5) LDX 2,PSP (6) второй элемент стека данных => X
TFR X,D (6) STD 2,PSP (6) D => во второй элемент стека
NEXT STX ,PSP (5) X => TOS
NEXT
ROT: LDX ,PSP (5) LDX ,PSP (5) TOS => X
STD ,PSP (5) LDD 2,PSP (6) второй со стека данных => D
LDD 2,PSP (6) STX 2,PSP (6) X => второй на стек
STX 2,PSP (6) LDX 4,PSP (6) третий со стека => X
NEXT STD 4,PSP (6) D => третий на стек
STX ,PSP (5) X => TOS
NEXT
0=: CMPD #0 LDD ,PSP TOS => D
BEQ TRUE CMPD #0 D равно нулю?
BEQ TRUE
FALSE: LDD #0 LDD #0 нет, = положить 0 в TOS
NEXT STD ,PSP
NEXT
TRUE: LDD #-1 LDD #-1 да, = положить -1 в TOS
NEXT STD ,PSP
NEXT
@: TFR TOS,W (6) LDD [,PSP] (8) извлечь D используя адрес в TOS
LDD ,W (5) STD ,PSP (5) D => TOS
NEXT NEXT
+: ADDD ,U++ PULU D вытолкнуть TOS в D
NEXT ADDD ,PSP добавить новый TOS к D
STD ,PSP сохранить D в TOS
NEXT
</pre>
! и +! медленнее работают в случае хранения TOS в регистре процессора:
<pre>
TOS в D TOS в памяти псевдокод
!: TFR TOS,W (6) PULU W (7) вытолкнуть адрес в W
PULU D (7) PULU D (7) вытолкнуть данные в D
STD ,W (5) STD ,W (5) сохранить данные по адресу
PULU TOS (7) NEXT
NEXT
+!: TFR TOS,W (6) PULU W (7) вытолкнуть адрес в W
PULU TOS (7) PULU D (7) вытолкнуть данные в D
ADDD ,W (6) ADDD ,W (6) добавить к D содержимое памяти
STD ,W (5) STD ,W (5) сохранить D в память
PULU TOS (7) NEXT
NEXT
</pre>
Причина медленности этих слов заключается в том, что они ожидают адрес на вершине стека данных, поэтому нужна дополнительная инструкция TFR. Поэтому удобно если TOS является адресным регистром. К сожалению, все адресные регистры 6809 процессора обсуждались, и более важно хранить W, IP, PSP, и RSP в них, чем TOS. Пенальти от размещения TOS в регистре процессора для операций ! +! будет перевешиваться выигрышем от множества арифметических и логических стековых операций.
Второй вариант: Intel 8051
Если 6809 это процессор мечты для Фортописателей, 8051 – кошмар. Он имеет только один адресный регистр общего назначения, и один режим адресации, который всегда использует 8-битный аккумулятор.
Все арифметические операции и много логических должны использовать аккумулятор. Есть только одна 16-битная операция INC DPTR. Аппаратный стек должен использовать 128-байтный накристальный файловый регистр. Такой процессор может вызвать язву [SIG92].
Некоторые 8051 Форты используют 16-битную модель [PAY90], но они на мой вкус слишком неторопливы. Давайте сделаем некоторые компромиссы и сделаем более быстрый Форт для 8051 процессора.
У нас есть только один адресный регистр. Поэтому давайте использовать в качестве счетчика команд родной регистр IP 8051 процессора, и выберем подпрограммный ШК. Если компилятор использует двухбайтные ACALL вместо трехбайтных LCALL-ов везде, где это возможно, большинство подпрограммного ШК будет занимать так же мало места, как и в случае с прямым и косвенным ШК.
Подпрограммный ШК предполагает, что в качестве указателя вершины стека возвратов используется аппаратный указатель стека. В 8051 64 ячейки пространства в накристальном регистровом файле, не достаточно места для хранения стеков в многозадачной системе. Поэтому вы можете:
a) ограничиться однозадачной Форт-системой;
b) писать таким образом все Форт-слова, что во время вызова они сохраняют адрес возврата в программный стек в ОЗУ; или
c) делать переключения задач с сохранением стека возвратов во внешнее ОЗУ.
Вариант b медленный! Перенос 128 байт при каждом переключении задач будет быстрее пересылки двух байт для каждого Форт-слова. Поэтому выберем вариант “a”, оставив открытой дверь для варианта “c” на будущее.
Один единственный действительно адресный регистр DPTR будет использоваться для многочисленных нужд. Он будет многоцелевым рабочим регистром W.
По правде, существует два других регистра, позволяющих адресовать внешнюю память: R0 и R1. Они работают только с 8битными адресами, старшие 8 бит явно выводятся в port 2. Но это допустимое ограничение для стеков, теперь они будут ограничены пространством в 256 байт. Давайте будем использовать R0 как указатель стека данных(PSP).
Это 256-байтное пространство может быть использовано под пользовательскую область.
Это делает P2 (второй порт) вторым байтом UP, и, подобно 6809, младший байт будет всегда нулем.
Так какая же программная модель на 8051 у нас получилась?
<pre>
Адрес имя назначение использования
0 R0 младший байт регистра PSP
1 R1
2 R2
3 R3
4 R4
5 R5
6 R6
7 R7
8-7Fh 120 байт стека возвратов
81h SP младший байт стека возвратов RSP (старший=00)
82-83h DPTR рабочий регистр W
A0h P2 старший байт регистров UP и PSP
E0h A
F0h B
</pre>
Замечу, что используется только нулевой регистровый банк. Дополнительные три регистровых банка с 08h по 1Fh, и побитно-адресуемый регион с 20h по 2Fh не используются в Форте. Использование нулевого банка оставляет наибольшее непрерывное пространство для стека возвратов. Позднее стек возвратов может быть уменьшен, если необходимо.
Подпрограммы NEXT, ENTER и EXIT не нужны на Форте с подпрограммным ШК.
Что насчет вершины стека данных? Регистров хватает, а операции с памятью на 8051 накладные. Давайте поместим TOS в R3:R2 (R3- старший байт на Интеловский манер).
Заметим, что B:A не будет использоваться – регистр А канал через который вся доступная память адресуется!
Гарвардские архитектуры
8051 использует Гарвардскую архитектуру: программа и данные хранятся в раздельных адресных пространствах. (Z8 и TMS320 два других примера.) 8051 – дегенеративный случай: нет физической возможности писать в память программ!
Это означает, что Фортописатель должен сделать одно из двух:
А) делать кросскомпиляцию всего, включая приложение, и распрощаться с надеждой на интерактивный Форт на 8051; или
b) отразить частично или полностью память программ в пространстве данных. Простейший путь – сделать полное перекрытие пространств с помощью внешнего «ИЛИ» над сигналами PSEN* и RD*.
Z8 и TMS320C25 более цивилизованные: они позволяют писать в память программ.
Третий вариант: Zilog Z80
Вариант Z80 очень полезный, потому что это экстремальный пример неортогонального процессора. Он имеет четыре различных вида адресных регистров! Некоторые операции используют A как приемник, другие - 8-битные регистры, третьи – HL, остальные любой 16-битный регистр… Много операций (подобных EX DE, HL ) определены только для одной комбинации регистров.
На подобных процессорах (Z80 или 8086), назначение Форт-функций должно быть осторожно сопоставлено с возможностями регистров. Необходимо рассмотреть множество альтернатив, и часто единственный путь – писать наброски кода для различных вариантов решений. Вместо рассмотрения бесконечных вариантов Форт-кода, я представлю одно распределение регистров, основанное на большом количестве экспериментов. Базовые принципы выбора я уже описал ранее.
Я хочу традиционный Форт, хотя я буду использовать прямой ШК. Все «классические» виртуальные регистры будут необходимы.
Игнорирую альтернативный набор регистров, Z80 имеет шесть адресных регистров со следующими возможностями:
BC,DE - LD A косвенно, INC, DEC
так же обмен DE/HL
HL - LD r косвенный, ALU косвенно, INC, DEC, ADD, ADC,
SBC, обмен с TOS, косвенный переход (JP)
IX,IY - LD r индексированный, ALU индексно, INC, DEC, ADD, ADC,
SBC, обмен с TOS, JP косвенно (все медленно)
SP - PUSH/POP 16-бит, ADD/ADC/SUB с HL/IX/IY
BC, DE, и HL так же могут модифицироваться 8битными кусками.
8-битный регистр А должен использоваться как временный регистр, так как является приемником для множества арифметических операций и режимов адресации памяти.
HL несомненно наиболее многоцелевой регистр он может использоваться как любой из Форт-виртуальных регистров. В любом случае, из-за того, что это единственный регистр позволяющий делать косвенный переход, HL должен использоваться как W (рабочий многоцелевой регистр).
IX и IY можно использовать как указатели стеков за счет их индексных режимов адресации, которые могут использоваться совместно с арифметическими операциями. Но возникает две проблемы: SP остается без работы, и IX/IY очень медленные! Большинство операций на обоих стеках производится над 16-битными числами. Это одна операция с использованием SP, но это требует четыре операции с использованием IX/IY. Один из Форт стеков будет использовать SP. И это должен быть стек данных, так как он используется более активно, нежели стек возвратов.
Что насчет Фортового IP? В основном IP извлекается из памяти и инкрементируется, поэтому нету преимуществ в использовании IX/IY вместо BC/DE. Но скорость существенна для IP, и BC/DE быстрее. Давайте поместим IP в DE : это имеет преимущество, так как DE может обмениваться с HL, что добавляет гибкости.
Вторая регистровая пара Z80 (в дополнение к W) будет нужна для 16-битной арифметики. Остается только BC, и он может использоваться для работы совместно с A в арифметических операциях. Но должен ли BC быть вторым рабочим регистром «Х» или вершиной стека? Только код может ответить, поэтому сейчас давайте оптимистично договоримся что BC = TOS.
Остаются несогласованными RSP и UP, а IX и IY не использованы. А раз IX и IY эквивалентны, пусть IX=RSP, а IY=UP.
Таким образом, соглашение по использованию регистров Z80 следующие:
<pre>
BC = TOS IX = RSP
DE = IP IY = UP
HL = W SP = PSP
</pre>
Теперь давайте посмотрим на NEXT для прямого ШК:
<pre>
DTC-NEXT: LD A,(DE) (7) (IP)->W, увеличить IP
LD L,A (4)
INC DE (6)
LD A,(DE) (7)
LD H,A (4)
INC DE (6)
JP (HL) (4) перейти на адрес, хранящийся в W
</pre>
Альтернативная версия (такое же число тактовых циклов)
<pre>
DTC-NEXT: EX DE,HL (4) (IP)->W, увеличить IP
NEXT-HL: LD E,(HL) (7)
INC HL (6)
LD D,(HL) (7)
INC HL (6)
EX DE,HL (4)
JP (HL) (4) перейти на адрес, хранимый в W
</pre>
Заметка: ячейки хранятся в обратном порядке, когда младший байт находится ближе к началу памяти. Так же, хотя может казаться выгодным хранить IP в HL, это не так потому что Z80 не может выполнять JP (DE). Использовать NEXT-HL будет короче.
Только для сравнения, давайте посмотрим на реализацию NEXT с использованием косвенного ШК. Приведенный ранее псевдокод требует наличия еще одного временного регистра «Х», содержимое которого может использоваться в косвенном переходе. Возьмем DE=X, а BC=IP, TOS будет храниться в ОЗУ.
<pre>
ITC-NEXT: LD A,(BC) (7) (IP)->W, увеличить IP
LD L,A (4)
INC BC (6)
LD A,(BC) (7)
LD H,A (4)
INC BC (6)
LD E,(HL) (7) (W)->X
INC HL (6)
LD D,(HL) (7)
EX DE,HL (4) перейти на адрес в X
JP (HL) (4)
</pre>
NEXT в прямом ШК имеет длину в 7 инструкций по сравнению с косвенным ШК, длиной в 11 инструкций. И косвенный ШК на Z80 теряет возможность хранить TOS в регистре. Мой выбор прямой ШК.
В случае использования прямого ШК в виде инлайн вставок код будет занимать по семь байт на слово. Переход на общий NEXT будет занимать только три байта, но это будет занимать дополнительные 10 тактовых циклов времени. Это другое компромиссное решение при создании ядра Форт-системы. Давайте выберем более быстрый инлайн вариант адресного интерпретатора NEXT. Иногда NEXT имеет еще больший размер, или памяти уж больно мало, тогда будет оправдан переход на NEXT.
Теперь давайте посмотрим на код слова ENTER. Используется CALL для получения адреса поля параметров с вершины стека возвратов:
<pre>
CALL ENTER (17)
...
ENTER: DEC IX (10) затолкнуть старое значение IP на стек возвратов
LD (IX+0),D (19)
DEC IX (10)
LD (IX+0),E (19)
POP DE (10) адрес поля параметров => IP
NEXT (38) ассемблерное макро на 7 инструкций
</pre>
Быстрее выполнять POP HL, и затем использовать последующие шесть инструкций NEXT
(минуя EX DE,HL):
<pre>
CALL ENTER (17)
...
ENTER: DEC IX (10) затолкнуть старое значение IP на стек возвратов
LD (IX+0),D (19)
DEC IX (10)
LD (IX+0),E (19)
POP HL (10) адрес поля параметров => HL
NEXT-HL (34) смотрите выше код NEXT для прямого ШК
119 всего циклов
</pre>
Когда используется JP, регистр W (HL) продолжает указывать на поле кода, а поле параметров находится на три байта дальше:
<pre>
JP ENTER (10)
...
ENTER: DEC IX (10) затолкнуть старое значение IP на стек возвратов
LD (IX+0),D (19)
DEC IX (10)
LD (IX+0),E (19)
INC HL ( 6) адрес поля параметров => IP
INC HL ( 6)
INC HL ( 6)
NEXT-HL (34)
120 циклов всего
</pre>
Итак, за счет альтернативной точки входа в NEXT , новое значение для IP не должно засылаться в регистровую пару DE.
Версия с CALL быстрее на один цикл. На встраиваемых Z80 однобайтная машинная команда RST может дать выигрыш в размере кода и скорости исполнения. Эта возможность не доступна на многих персональных компьютерах на Z80.
Четвертый вариант: Intel 8086
I8086 – другой поучительный процессор. Вместо проектирования собственного варианта давайте взглянем на один из новых шараварных Фортов для IBM PC: Pygmy Forth [SER90].
Pygmy это Форт основанный на прямом шитом коде с хранением верхнего элемента стека данных в регистре. Использование регистров 8086 процессора следующее:
<pre>
AX = W DI = свободный
BX = TOS SI = IP
CX = свободный BP = RSP
DX = свободный SP = PSP
</pre>
Большинство i8086 Фортов использует регистр SI как указатель интерпретации IP, таким образом NEXT может быть написан с использованием машинной инструкции LODSW. В Pygmy NEXT (прямой ШК) выглядит так:
<pre>
NEXT: LODSW
JMP AX
</pre>
Это достаточно коротко для использования этого кода в виде инлайн вставок.
Высокоуровневые Форт –слова используют JMP (относительный) на их машинный код. Подпрограмма ENTER (называемая 'docol' в Pygmy) должна брать адрес поля параметров из W:
<pre>
ENTER: XCHG SP,BP
PUSH SI
XCHG SP,BP
ADD AX,3 aдрес поля параметров => IP
MOV SI,AX
NEXT
</pre>
Заметка: используйте XCHG для обмена двух указателей стека. Это позволяет использовать команды PUSH и POP для обоих стеков, что быстрее косвенного доступа.
<pre>
EXIT: XCHG SP,BP
POP SI
XCHG SP,BP
NEXT
</pre>
Сегментная модель
Pygmy – односегментный Форт. Весь код и данные содержатся в пределах единого 64Кбайтного сегмента. (это “tiny” модель в Турбо Си). Все стандарты Форта описывают модель в которой все хранится в едином адресном пространстве, доступном одними и теми же операциями чтения и записи.
Однако появляются IBM PC Форты, использующие множество сегментов – вплоть до пяти различных видов данных [KEL92,SEY89]. Пример:
CODE ... машинный код
LIST ... выскоуровневый код (определения слов)
HEAD ... заголовки для всех Форт-слов
STACK ...стек данных и стек возвратов
DATA ... переменные и пользовательские данные
Это позволяет PC Фортам отодвинуть 64Кбайтный предел памяти без накладок на реализацию 32-битного Форта на 16-битном процессоре. Реализация многосегментной модели находится за пределами этой статьи.
Оппа!
В моих дизайнерских решениях для 6809 в предыдущей части колоссальная ошибка. Это стало очевидно, когда я взялся кодировать слово EXECUTE. Execute вызывает исполнение Форт-слова, чей адрес находится на вершине стека данных (точнее: адрес компиляции, называемый адресом поля кода, находится на вершине стека данных). Это может быть любое Форт-слово, в том числе константа или переменная. Это отличается от обычного процесса адресной интерпретации, в котором адрес слова, которое надо исполнять хранится в IP.
В нашем прямом ШК для 6809 это легко может быть закодировано так:
<pre>
EXECUTE: TFR TOS,W положить исполнимый адрес слова в регистр W
PULU TOS извлечь новое значение TOS со стека данных
JMP ,W перейти на адрес, хранимый в W
</pre>
Заметка: это JMP ,W а не JMP [,W], так как мы уже имеем адрес поля кода слова. Мы не извлекаем этот адрес из шитого кода. Если TOS не в регистре, EXECUTE может быть выполнено с помощью простого JMP [,PSP++].
Теперь, допустим, что это исполняемое слово определено через двоеточие, а W будет указывать на его поле кода, которое содержит JMP ENTER. Это приведет к следующему (описано выше):
<pre>
JMP ENTER
...
ENTER: PSHS IP
LDX -2,IP перечитать адрес поля кода
LEAY 3,X
NEXT
</pre>
Это ошибка! Мы не исполняем это слово изнутри ШК, и поэтому IP не указывает на копию адреса поля кода(помните, адрес исполняемого слова пришел со стека данных)! Этот вариант ENTER не будет работать совместно с EXECUTE, потому что нет возможности найти адрес исполнимого слова!
Это выдвигает новое важное правило для Фортов с прямым ШК: если NEXT не оставляет адрес исполнимого слова в регистре, вы должны использовать CALL в поле кода вызываемого слова.
Таким образом, в 6809 возвращаем использование JSR в поле кода. Но для избегания потери скорости для ENTER, который является одним из наиболее используемых фрагментов кода в Форте, я завершу «пример для студента» из начала этой статьи. Отметьте, что случается если вы обмениваете регистры назначенные для RSP и PSP.
<pre>
c RSP=S, c RSP=U,
и PSP=U и PSP=S
(старый) (новый)
JSR ENTER JSR ENTER
... ...
ENTER: PULS W PSHU IP затолкнуть старый IP на стек возвратов
PSHS IP PULS IP извлечь новый IP из стека данных
TFR W,IP NEXT
NEXT
</pre>
Новая версия исполняется за 31 цикл, столько же, сколько JMP версия, которую я хотел использовать. Улучшение, потому что JSR версия ENTER должна использовать и стек возвратов и 6809 подпрограммный стек возвратов. Использование двух различных стековых указателей означает, что мы не должны обменивать TOS с IP, убирается необходимость во временном регистре.
Это иллюстрирует обычный процесс разработки нового ядра Форт-системы: делаем некоторые системные соглашения, пишем пример кода, находим ошибку или лучший путь решения проблемы, выкидываем часть кода, меняем некоторые системные соглашения, переписываем часть кода, и зацикливаемся в этом процессе до нахождения удовлетворяющего нас решения.
Это важный урок: набрасывайте EXECUTE в тестовый набор слов!
Оппа, опять
Карли Блудворт указал на незначительную, но досадную ошибку в моем 6809 коде. В версии с хранением вершины стека в памяти версия слова 0= , я приведу фрагмент кода
<pre>
LDD ,PSP
CMPD #0
</pre>
на тестирование вершины стека на равенство нулю. В этом случае CMPD инструкция лишняя, так как LDD инструкция установит zero флаг если D равен нулю (в случае варианта TOS в регистре D продолжает требовать CMPD инструкцию, но остается быстрее, чем TOS в памяти).
Литература:
[KEL92] Kelly, Guy M., "Forth Systems Comparisons," Forth Dimensions XIII:6 (Mar/Apr 1992). Also published in the 1991 FORML Conference Proceedings. Both available from the Forth Interest Group, P.O. Box 2154, Oakland, CA 94621. Illustrates design tradeoffs of many 8086 Forths with code fragments and benchmarks -- highly recommended!
[MOT83] Motorola Inc., 8-Bit Microprocessor and Peripheral Data, Motorola data book (1983).
[SIG92] Signetics Inc., 80C51-Based 8-Bit Microcontrollers, Signetics data book (1992).
Форт-системы
[PAY90] Payne, William H., Embedded Controller FORTH for the 8051 Family, Academic Press (1990), ISBN 0-12-547570-5. This is a complete "kit" for a 8051 Forth, including a metacompiler for the IBM PC. Hardcopy only; files can be downloaded from GEnie. Not for the novice!
[SER90] Sergeant, Frank, Pygmy Forth for the IBM PC, version 1.3 (1990). Distributed by the author, available from the Forth Interest Group. Version 1.4 is now available on GEnie, and worth the extra effort to obtain.
[SEY89] Seywerd, H., Elehew, W. R., and Caven, P., LOVE-83Forth for the IBM PC, version 1.20 (1989). A shareware Forth using a five-segment model. Contact Seywerd Associates, 265 Scarboro Cres., Scarborough, Ontario M1M 2J7 Canada.