Шапка

6.3 Изучение частот появления слов языка FORTH

Итак, выяснив разницу между стековыми машинами и компьютерами других архитектур, можно перейти к численным результатам, которые показывают, как стековые машины работают. Результаты измерений частот появления тех или иных инструкций и размеров кода для стековых и регистровых машин прилагается ( ссылки включают: Blake ( 1977 ), Cooke & Donde ( 1982 ), Cooke & Lee ( 1980 ), Cragon ( 1979 ), Haikala ( 1982 ), McDaniel ( 1982 ), Sweet & Sandman ( 1982 ), и Tanenbaum ( 1978 )). К сожалению, большая часть таких измерений проводилась для программ, написанных на обычных, а не стековых языках, подобных FORTH. Впервые статистика исполнения программ на FORTH была опубликована Hayes et al. ( 1987 ) и данная книга её слегка расширит.

Все сведения, приведённые в этой части, получены при тестировании программ на языке FORTH, так как только такие программы способны наилучшим образом выявить возможности стековых машин. Все предупреждения о тестировании по-прежнему актуальны и приведённые результаты должны использоваться только как некое приближение к «истине» ( что бы это слово ни обозначало ).

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

Frac : программа заполнения пространства фракталами, использующая генератор случайных чисел. Для единообразия условий рисования изображения программа запускается с одними и теми же начальными параметрами ( Koopman 1987e , Koopman 1987f ).

Life : простая реализация игры Джона Конвея «Жизнь» на экране размером 80x25 символов. За каждый запуск программа проигрывает десять поколений заполненного глайдерами экрана.

Math : библиотека 32-разрядных вычислений с плавающей запятой, целиком написанная на языке FORTH без использования машиннозависимой оптимизации. При каждом запуске программа вычисляет таблицу синусов, косинусов и тангенсов для десяти целочисленных значений угла в диапазоне от 1 до 10° ( Koopman 1985 ).

Compile : командный файл для компиляции нескольких программ на языке FORTH. Используется для измерения производительности самого компилятора.

Fib : вычисление 24-го числа Фибоначчи по рекурсивному алгоритму ( часто называемому «тупым» ).

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

Queens : задача о N ферзях ( ведущая своё происхождение от головоломки о 8 ферзях на шахматной доске ), написанная в виде рекурсивной процедуры. Программа находит первое удовлетворяющее условиям расположение N ферзей на доске размером NxN . При каждом вызове вычисляется положение при N равном 12 .

Три программы из перечисленных обеспечивают наилучшее сочетание различных аспектов программирования. Первая - «Math», которая интенсивно использует 16-разрядный стек при работе с 32-разрядными величинами ( и 48-разрядным промежуточным форматом с плавающей запятой ). Вторая - «Life», которая активно работает с массивами ячеек памяти и выполняет множество условных переходов. Третья - «Frac», которая рисует линию и элементарные графические проекции.

Замеры производительности при компиляции полезны тем, что отражают активность при разборе входного потока символов и поиска идентификаторов.

6.3.1 «Динамическая» частота появления инструкций

Табл. 6.1 показывает «динамическую» частоту появления для самых интенсивно исполняемых примитивов языка для программ «Frac», «Life», «Math» и «Compile». Динамической частотой инструкции называется число исполнений этой инструкции в ходе работы программы [* то есть частоту появления их в потоке команд при исполнении программы; есть ещё «статическая» - частота появления команды в исходном тексте ] . Полную версию таблицы можно посмотреть в Приложении _C  , табл. C.1 . Колонка «AVE» показывает средневзвешенное значение примитива для всех четырёх программ и является хорошим приближением к частоте появления в большинстве программ на языке FORTH. В таблицу включены слова, входящие в десятку наиболее употребительных из колонки «AVE» или в первый десяток для конкретной программы. Например, «EXECUTE» имеет средний ( «AVE» ) показатель всего 0.65%, но 2.45% для программы «Compile» и входит в десяток самых употребительных для неё.

Табл. 6.1. Частота появления примитивов FORTH в потоке исполняемых команд

 
FRAC
LIFE
MATH
COMPILE
AVE
xxxxxxxxxx
XX.XXX
XX.XXX
XX.XXX
XX.XXX
XX.XXX
CALL 11.16% 12.73% 12.59% 12.36% 12.21%
EXIT 11.07% 12.72% 12.55% 10.60% 11.74%
VARIABLE 7.63% 10.30% 2.26% 1.65% 5.46%
@ 7.49% 2.05% 0.96% 11.09% 5.40%
0BRANCH 3.39% 6.38% 3.23% 6.11% 4.78%
LIT 3.94% 5.22% 4.92% 4.09% 4.54%
+ 3.41% 10.45% 0.60% 2.26% 4.18%
SWAP 4.43% 2.99% 7.00% 1.17% 3.90%
R> 2.05% 0.00% 11.28% 2.23% 3.89%
>R 2.05% 0.00% 11.28% 2.16% 3.87%
CONSTANT 3.92% 3.50% 2.78% 4.50% 3.68%
DUP 4.08% 0.45% 1.88% 5.78% 3.05%
ROT 4.05% 0.00% 4.61% 0.48% 2.29%
USER 0.07% 0.00% 0.06% 8.59% 2.18%
C@ 0.00% 7.52% 0.01% 0.36% 1.97%
I 0.58% 6.66% 0.01% 0.23% 1.87%
= 0.33% 4.48% 0.01% 1.87% 1.67%
AND 0.17% 3.12% 3.14% 0.04% 1.61%
BRANCH 1.61% 1.57% 0.72% 2.26% 1.54%
EXECUTE 0.14% 0.00% 0.02% 2.45% 0.65%
Всего инструкций 2051600 1296143 6133519 447050

Первый и очевидный вывод: среди операций доминируют вызовы подпрограмм и выходы из них. Этот широко известный факт объясняет всё то внимание, которое FORTH-ориентированные стековые процессоры уделяют совмещению вызовов и выходов из подпрограмм с другими операциями. Число команд выхода «EXIT» несколько меньше, чем число вызовов «CALL», так как некоторые слова FORTH удаляют элементы из стека возврата, объединяя два уровня вызова подпрограмм. Это приводит к условному досрочному выходу из вызывающей подпрограммы.

Интерес представляет и время, затрачиваемое словами, которые тасуют элементы в стеке. Суммарная цифра по всем таким командам составляет около 25%. На первый взгляд довольно много. С другой стороны, все стековые процессоры могут совмещать реорганизацию содержимого стека с другой полезной работой ( подобной комбинации «OVER -» ), а значит, полученная цифра гораздо больше, чем встречающиеся на практике значения. Кроме того, в полученных 25% пятую часть занимает очень активное использование слов «>R» и «R>» в библиотеке математики с плавающей запятой при работе с 32-битными величинами. Эти издержки отсутствуют в 32-разрядных процессорах или в 16-разрядных машинах, имеющих память с быстрым доступом для хранения промежуточных результатов ( таких как NC4016 и RTX 2000 ).

Любопытна и очень важная задача размещения для последующего использования данных в стеке ( в число примитивов входят «VARIABLE», «@», «LIT», «CONSTANT» и «USER» ). К счастью, в стековых машинах и эти действия можно совмещать с другими операциями.

И, наконец, заглянув в Приложение _C  , можно обнаружить, что множество инструкций имеют динамическую частоту появления менее 1%. Тем не менее, немедленно исключать их как ненужные не стоит, потому что при отсутствии аппаратной поддержки исполнение многих из них потребует много времени. Для выяснения степени важности инструкции одной частоты появления недостаточно.

6.3.2 «Статическая» частота появления инструкций

В табл. 6.2 приведены «статические» частоты появления инструкций , встречающихся при компиляции программ «Frac», «Life» и «Math» и при работе программы «Compile» ( через неё прогонялись «Frac», «Queens», «Hanoi» и «Fib» ). Статической частотой инструкции называется число появлений этой инструкции в исходном тексте программы. Колонка «AVE» показывает средневзвешенное значение для четырёх остальных значений, являющихся хорошим приближением к частоте компиляции для большинства программ на языке FORTH. В таблицу включены слова, входящие в десятку наиболее употребительных для колонки «AVE» или в первый десяток для конкретной программы.

Табл. 6.2. Частота появления важных примитивов FORTH в текстах программ ( полную версию см. в табл. C.2 )

 
FRAC
LIFE
MATH
COMPILE
AVE
xxxxxxxxxx
XX.XXX
XX.XXX
XX.XXX
XX.XXX
XX.XXX
CALL 16.82% 31.44% 37.61% 17.62% 25.87%
LIT 11.35% 7.22% 11.02% 8.03% 9.41%
EXIT 5.75% 7.22% 9.90% 7.00% 7.47%
@ 10.81% 1.27% 1.40% 8.88% 5.59%
DUP 4.38% 1.70% 2.84% 4.18% 3.28%
0BRANCH 3.01% 2.55% 3.67% 3.16% 3.10%
PICK 6.29% 0.00% 1.04% 4.53% 2.97%
+ 3.28% 2.97% 0.76% 4.61% 2.90%
SWAP 1.78% 5.10% 1.19% 3.16% 2.81%
OVER 2.05% 5.10% 0.76% 2.05% 2.49%
! 3.28% 2.12% 0.90% 2.99% 2.32%
I 1.37% 5.10% 0.11% 1.62% 2.05%
DROP 2.60% 0.85% 1.69% 2.31% 1.86%
BRANCH 1.92% 0.85% 2.09% 2.05% 1.73%
>R 0.55% 0.00% 4.11% 0.77% 1.36%
R> 0.55% 0.00% 4.68% 0.77% 1.50%
C@ 0.00% 3.40% 0.61% 0.34% 1.09%
= 0.14% 2.76% 0.29% 0.26% 0.86%
Всего инструкций 731 471 2777 1171

При статических измерениях очень частыми становятся вызовы подпрограмм - каждая четвёртая компилируемая инструкция. Отметим, что «Frac» посчитана дважды, так как включается ещё и в «Compile», и, на самом деле число вызовов чуть меньше.

6.3.3 Сжатие инструкций в RTX 32P

Если команды выхода из подпрограмм используются столь активно, то не стоит удивляться, что в большинстве стековых машин есть механизмы совмещения выхода из процедуры с другими операциями. Есть ещё одно важное дополнительное наблюдение - в исходных текстах вызовы подпрограмм встречаются чаще, чем выходы из них [* одна из причин отмечалась выше ] , и привлекательнее для комбинирования с другими операциями.

RTX 32P , обсуждавшийся в §5.3 , уникален тем, что только у него есть единый формат, совмещающий код операции и адрес перехода или вызова подпрограммы. На первый взгляд это кажется зряшной тратой памяти, но её цена относительно невелика, а возможный прирост производительности очень заметен. К сожалению, единый формат инструкций подходит только для 32-разрядных процессоров, так как 16 бит недостаточно для хранения кода операции и большого поля адреса.

В таблицах ниже показаны результаты вычислительной и компиляционной статистики, набранной на переписанных с учётом преимуществ RTX 32P версиях «Frac», «Life» и «Math».

6.3.3.1 Увеличение скорости исполнения

В табл. 6.3a , 6.3b , 6.3c и 6.3d представлены результаты измерения частоты выполнения различных инструкций на RTX 32P для программы, собранной с четырьмя различными уровнями оптимизации. Часть (a) таблицы показывает результаты исполнения без сжатия инструкций и подпрограмм и без локальной оптимизации кода ( комбинирования соседних инструкций ).

Табл. 6.3(a) Сжатие инструкций ВЫКЛЮЧЕНО, комбинирование кодов операций ВЫКЛЮЧЕНО

 
FRAC
LIFE
MATH
AVE
xxxxxxxxxx
XX.XXX
XX.XXX
XX.XXX
XX.XXX
OP 57.54% 46.07% 49.66% 51%
CALL 19.01% 26.44% 19.96% 22%
EXIT 10.80% 12.53% 16.25% 13%
OP+CALL 0.00% 0.00% 0.00% 0%
OP+EXIT 0.00% 0.00% 0.00% 0%
CALL+EXIT 0.00% 0.00% 0.00% 0%
OP+CALL+EXIT 0.00% 0.00% 0.00% 0%
COND 5.89% 9.95% 6.56% 7%
LIT 6.76% 5.01% 7.57% 6%
LIT-OP 0.00% 0.00% 0.00% 0%
VARIABLE-OP 0.00% 0.00% 0.00% 0%
VARIABLE-OP-OP 0.00% 0.00% 0.00% 0%
Всего инструкций 8381513 1262079 940448
OP-OP 0.00% 0.00% 0.00% 0%

Часть (b) показывает эффект от упаковки часто встречающихся кодовых последовательностей ( подобных «SWAP DROP», «OVER +», «Variable @» и «Variable @ +» ) в одну инструкцию. Ряд, помеченный «OP-OP», показывает число парных сочетаний, которые рассматриваются как один опкод в графах «OP», «OP-CALL», «OP-EXIT» и «OP-CALL-EXIT». Все специальные случаи появления пар «LITERAL +» , «LITERAL AND» и т.д. упомянуты в виде «LITERAL-OP». «VARIABLE-OP» включает комбинации «Variable @» и «Variable !», а «VARIABLE-OP-OP» объединяет «Variable @ +» и «Variable @ -». Все особые случаи констант и переменных требуют использования отдельной инструкции для хранения кода операции и адреса и не могут комбинироваться с другими директивами. Для тестовых программ сокращение числа исполняемых команд при локальной оптимизации может достигать 10%.

Табл. 6.3(b) Сжатие инструкций ВЫКЛЮЧЕНО, комбинирование кодов операций ВКЛЮЧЕНО

 
FRAC
LIFE
MATH
AVE
xxxxxxxxxx
XX.XXX
XX.XXX
XX.XXX
XX.XXX
OP 50.92% 42.22% 45.94% 46%
CALL 17.81% 28.31% 21.42% 23%
EXIT 12.48% 13.42% 17.45% 14%
OP+CALL 0.00% 0.00% 0.00% 0%
OP+EXIT 0.00% 0.00% 0.00% 0%
CALL+EXIT 0.00% 0.00% 0.00% 0%
OP+CALL+EXIT 0.00% 0.00% 0.00% 0%
COND 6.82% 10.66% 7.05% 8%
LIT 2.60% 1.94% 2.53% 2%
LIT-OP 5.21% 3.43% 5.59% 5%
VARIABLE-OP 2.67% 0.00% 0.01% 1%
VARIABLE-OP-OP 1.49% 0.00% 0.01% 1%
Всего инструкций 7250149 1178235 875882
OP-OP 4.72% 3.68% 1.76% 3%

Часть (c) показывает результаты замены локальной оптимизации на сжатие инструкций. Это означает, что везде, где это возможно, операции комбинируются с последующими директивами вызова подпрограмм и выхода из них. Пара инструкций вызов-выход преобразуются в безусловный переход для исключения концевой рекурсии. В итоге комбинации операций и вызовов/выходов из подпрограмм составляют до 24% всех инструкций. Иначе говоря, «бесплатно» будут выполнены около 40% исходных вызовов процедур. Почти все выходы из них выполняются в дополнение к другим операциям. Исключение составляют лишь специальные инструкции, вроде работы с константами или указателем стека возврата, которые нельзя объединить с выходом из подпрограммы.

Табл. 6.3(c) Сжатие инструкций ВКЛЮЧЕНО, комбинирование кодов операций ВЫКЛЮЧЕНО

 
FRAC
LIFE
MATH
AVE
xxxxxxxxxx
XX.XXX
XX.XXX
XX.XXX
XX.XXX
OP 48.84% 31.26% 40.81% 40%
CALL 8.46% 22.20% 15.53% 15%
EXIT 4.57% 0.00% 4.80% 3%
OP+CALL 13.93% 11.47% 6.68% 11%
OP+EXIT 7.71% 15.96% 12.90% 12%
CALL+EXIT 0.80% 0.00% 2.04% 1%
OP+CALL+EXIT 0.15% 0.00% 0.03% 0%
COND 7.23% 12.69% 7.99% 9%
LIT 8.31% 6.39% 9.22% 8%
LIT-OP 0.00% 0.00% 0.00% 0%
VARIABLE-OP 0.00% 0.00% 0.00% 0%
VARIABLE-OP-OP 0.00% 0.00% 0.00% 0%
Всего инструкций 6827482 990313 772865
OP-OP 0.00% 0.00% 0.00% 0%

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

Табл. 6.3(d) Сжатие инструкций ВКЛЮЧЕНО, комбинирование кодов операций ВКЛЮЧЕНО

 
FRAC
LIFE
MATH
AVE
xxxxxxxxxx
XX.XXX
XX.XXX
XX.XXX
XX.XXX
OP 39.05% 24.91% 39.19% 34%
CALL 6.75% 24.27% 15.94% 16%
EXIT 6.54% 0.01% 10.78% 6%
OP+CALL 12.71% 12.53% 6.87% 11%
OP+EXIT 6.78% 17.44% 7.40% 11%
CALL+EXIT 0.95% 0.01% 2.10% 1%
OP+CALL+EXIT 0.09% 0.00% 0.03% 0%
COND 7.84% 13.86% 8.21% 10%
LIT 3.00% 2.52% 2.95% 3%
LIT-OP 6.00% 4.45% 6.51% 6%
VARIABLE-OP 3.08% 0.00% 0.01% 1%
VARIABLE-OP-OP 1.72% 0.00% 0.01% 1%
Всего инструкций 6294109 906469 752257
OP-OP 5.44% 4.79% 2.05% 4%

Результаты измерений показывают, что число инструкций для библиотеки «Math» уменьшилось с 6.1 миллиона для 16-разрядной системы до 940 тысяч для RTX 32P, указывая на необходимость 32-разрядного процессора для вычислений с плавающей запятой. Цифры для «Life» ( состоящей по большей части из работы с 8-разрядными данными ) почти одинаковы для обеих систем. Данные для «Frac» демонстрируют четырёхкратное увеличение числа инструкций, вызванное более высоким графическим разрешением 32-разрядной версии, которое требует вычисления в четыре раза большего числа точек и, соответственно, приблизительно в четыре раза большего числа инструкций.

6.3.3.2 Цена вопроса в терминах расхода памяти

Увеличение производительности от комбинирования операций с вызовами подпрограмм выгодно, так как фактически не использует дополнительные ресурсы внутри процессора. На самом деле из-за использования единственного формата инструкций конструкция процессора упрощается. Остаётся вопрос о влиянии таких технических решений на использование памяти.

К счастью, программы на языке FORTH имеют частоту статического появления вызовов подпрограмм [* присутствия в коде ]   даже большую, чем частота динамического появления [* исполнения ] . Это обеспечивает широкие возможности для их комбинирования с другими операциями. Табл. 6.4a и 6.4b показывает разницу статического объёма исходной программы по сравнению с объёмом после компрессии и локальной оптимизации.

RTX 32P использует 9-разрядные коды операций, 21-разрядные адреса и 2-битное поле управления. Если предположить, что мы можем использовать оптимально упакованные инструкции, то можно создать формат операций, имеющий 11 бит, включая бит-признак выхода из подпрограммы, и 23-битный формат вызовов/переходов. Кроме того, без стеснения заявим, что все выходы из подпрограмм всегда комбинируются с другими операциями или вызовы заменяются переходами. Всё перечисленное предполагает создание машины с переменной шириной слова ( 11-битными кодами операций и 23-битными вызовами подпрограмм, что есть задача нетривиальная ), мы же просто хотим узнать теоретический минимум.

Три оптимизированных программы состоят из 1953 11-битных кодов, 1389 23-битных вызовов подпрограмм и 565 34-битных комбинаций. Всё вместе занимает 72640 бит.

Теперь рассмотрим реальные программы, транслированные для RTX 32P с применением оптимизации. Учитывая фиксированный 32-битный формат, имеющий не всегда используемые поля, получаем общий объём 3300 инструкций равным 105600 бит. Таким образом, увеличение расхода памяти - стоимость в единицах её объёма - равно 32900 бит или на 31% больше теоретического минимума.

Табл. 6.4(a) Сжатие инструкций ВЫКЛЮЧЕНО, комбинирование кодов операций ВЫКЛЮЧЕНО

 
FRAC
LIFE
MATH
AVE
xxxxxxxxxx
XX.XXX
XX.XXX
XX.XXX
XX.XXX
OP 48.40% 51.46% 44.72% 48%
CALL 28.48% 33.01% 35.64% 32%
EXIT 5.12% 6.41% 7.55% 6%
OP+CALL 0.00% 0.00% 0.00% 0%
OP+EXIT 0.00% 0.00% 0.00% 0%
CALL+EXIT 0.00% 0.00% 0.00% 0%
OP+CALL+EXIT 0.00% 0.00% 0.00% 0%
COND 3.52% 4.46% 4.04% 4%
LIT 14.48% 4.66% 8.05% 9%
LIT-OP 0.00% 0.00% 0.00% 0%
VARIABLE-OP 0.00% 0.00% 0.00% 0%
VARIABLE-OP-OP 0.00% 0.00% 0.00% 0%
Всего инструкций 1250 515 2422
OP-OP 0.00% 0.00% 0.00% 0%

Табл. 6.4(b) Сжатие инструкций ВКЛЮЧЕНО, комбинирование кодов операций ВКЛЮЧЕНО

 
FRAC
LIFE
MATH
AVE
xxxxxxxxxx
XX.XXX
XX.XXX
XX.XXX
XX.XXX
OP 33.71% 35.78% 37.05% 36%
CALL 17.33% 21.94% 27.03% 22%
EXIT 1.47% 2.87% 2.39% 2%
OP+CALL 11.65% 21.15% 10.54% 14%
OP+EXIT 3.78% 4.70% 1.73% 3%
CALL+EXIT 1.05% 1.04% 4.02% 2%
OP+CALL+EXIT 0.42% 0.00% 1.17% 1%
COND 4.62% 6.00% 4.98% 5%
LIT 16.17% 4.18% 8.61% 10%
LIT-OP 2.83% 2.08% 1.32% 2%
VARIABLE-OP 5.46% 0.26% 1.01% 2%
VARIABLE-OP-OP 1.47% 0.00% 0.15% 1%
Всего инструкций 952 383 1965
OP-OP 2.73% 5.22% 1.98% 3%

При более внимательном взгляде можно обнаружить, что 766 неиспользуемых 9-разрядных полей кода операции и 917 неиспользуемых 23-разрядных полей адреса дают в сумме 27985 бит или 27% превышения расхода памяти [* с учётом того, что единый формат занимает 9+21+2=32 бита, а раздельный «теоретический» 11+23=34 бита ]   при 25% уменьшении числа исполняемых инструкций [* «OP-OP»: 952 + 383 + 1965 = 3300 ( со сжатием ) против 1250 + 515 + 2422 = 4187 ( без сжатия )] . Таким образом, существенный прирост скорости получен при относительно скромных затратах даже по сравнению с форматом инструкций переменной ширины.

У представленных здесь расчётов есть небольшой недостаток: они проводились на нескольких относительно небольших примерах. В них выполняются некоторые довольно сложные действия и проведённые измерения могут создать впечатление, что программы для стековых машин много места не занимают, но трудность заключается в том, что большую программу на языке FORTH найти сложно [* !!! Больших проектов на FORTH не существует ] . Так или иначе, выбранные примеры имеют разумное покрытие словаря FORTH и по ответственному мнению автора результаты близки к тому, что можно получить, используя программы большего размера.

Одним из возможных путей получения заметно больших программ было бы использование результатов компиляции текстов на традиционных языках, но такой код имел бы существенно иные характеристики, потому что методы решения проблем с использованием Си и FORTRAN существенно отличаются от таковых при использовании FORTH. Этот тезис будет дополнительно рассмотрен в Части _7  .

Previous part:

Next part: