Основы оптимизации под Z80 Существует 4 направления оптимизации:по
размеру, по скорости, по сжатому размеру и
по объёму исходника. На последнем останав─
ливаться здесь не будем.
Оптимизация по размеру
1.
Если структура программы определена,
следует подсчитать количество обращений к
каждой процедуре (это очень легко - изме─
нить метку её названия и попытаться оттра─
нслировать), а на основании этого числа и
её,процедуры,размера можно решить вопрос о
лишении её статуса подпрограммы и внесении
в вызывающий фрагмент. Иногда подпрограмма
от этого не выигрывает. Например, в случае
множества условных возвратов (они короче
условных переходов) в ней.
CALL addr : RET заменяется на JP addr
или, если возможно, JR addr. Ещё лучше пе─
ренести фрагмент с JP к указанному в нём
адресу.
CALL addr:JP addr2 упрощается аналогич─
но: переносом вплотную к указанному в CALL
адресу и заменой упомянутой конструкции на
LD BC,addr2:PUSH BC.
Крупный цикл наподобие опроса клавиш -
с большим количеством переходов на одну и
ту же точку - логично сократить так же,
занося адрес точки на стек и пользуясь ус─
ловными возвратами.
Сложную систему условий часто удаётся
привести к какому-либо виду, содержащему
таблицу, возможно, обрабатываемую командой
CPIR.
Окончания циклов вида JR NZ,exit:CALL
addr:JR Z,loop:exit приводятся к виду CALL
Z,addr:JR Z,loop. Иногда для этого прихо─
дится менять признаки завершения в подпро─
грамме addr, а подчас и создавать эту под─
программу, если её нет :)
Разумным распределением соглашений о
флагах на выходе из разных процедур (не─
плохо использовать факты, что п/п такая-то
всегда устанавливает CY, а другая п/п пре─
дпочитает сбрасывать Z ). можно добиться
более частого использования условных вызо─
вов и сэкономить тем самым немало байт и
строчек кода.
Полезно время от времени прогуляться по
исходнику и записать самые популярные фра─
гменты вызова некоторых процедур. Часто
перед CALL стоят однотипные команды. Это
позволяет внести их в начало процедуры,по─
льзуясь тем, что в ассемблере,в отличие от
ЯВУ, возможно сколько угодно точек входа в
процедуру! В конце концов процедуры склеи─
ваются в ёлки наподобие такой:
OUT0 LD A,16
JR $+4
OUT7 LD A,23
OUTME LD (pg),A
OUTNO PUSH BC
LD BC,32765
OUT (C),A
POP BC
RET
И это ещё не самый крупный экземпляр!:)
Если же,например,после CALL часто стоит
какая-либо операция, которая в других слу─
чаях безвредна (что-то вроде OR A или LD
HL,(addr) ), можно подставить эту операцию
в конец самой процедуры. Бывают случаи,ко─
гда ничего подставить нельзя,но оказывает─
ся выгодным сделать промежуточную вызывал─
ку процедуры - с самыми частыми командами,
сопутствующими этому вызову.
Приведённые рекомендации относятся к
организации программы и почти не касаются
её внутреннего устройства. На следующей
странице мы поговорим и об устройстве. По─
путно отметим здесь, что существует способ
программирования без использования команд
CALL, а также средство экономии памяти пу─
тём создания интерпретируемой или компили─
руемой системы команд (скрипта).
2.
Есть ряд конструкций, неоптимальных с
точки зрения размера / скорости / регист─
ров практически всегда.Вот несколько таких
"запрещённых конструкций":
SLA A (заменяется на ADD A,A );
SLA L:RL H (заменяется на ADD HL,HL );
RL L:RL H (заменяется на ADC HL,HL );
Два последних совета можно применить к
другим похожим случаям, если перераспреде─
лить регистры;
Полная альтернатива вида
JR C,cy
LD reg,число
JR ok
cy LD reg,число2
ok
(заменяется на LD reg,число2: JR C,ok:
LD reg,число: ok или, если reg - аккумуля─
тор: SBC A,A:AND число!число2:XOR число );
PUSH HL:SBC HL,DE:POP HL (заменяется на
SBC HL,DE : ADD HL,DE - проверьте: флаги в
том же состоянии!);
DEC B:JR NZ,loop (заменяется на DJNZ,
если не важен флаг Z ) - можно перераспре─
делить регистры ради такой оптимизации;
LD L,(IX)
INC HX
LD H,(IX)
DEC HX
INC IX
(заменяется на LD L,(IX-128):INC IX:LD H,
(IX+127) с другим нач. значением IX );
LD A,reg:OR A (кроме индексных; заменя─
ется на INC reg:DEC reg );
В дальнейшем вы найдёте и другие.
Попадаются возможности избежать счётчи─
ка цикла, если выходить по переполнению
младшего байта адреса: INC L:JR NZ,loop.
Многие условные вычисления, связанные с
инкрементом (и не только), удобнее органи─
зовать арифметически.
Самым эффективным способом размещать
переменные программы почти всегда будут
встроенные в текст переменные вида:
var1=$+1
LD DE,0
Из всех мест, где переменная упоминает─
ся, для этого следует выбирать место,в ко─
тором выигрыш максимален (например, где
рассматриваемую переменную вычитают).
Другой вариант размещения - в блоке си─
стемных переменных Бейсика (IY+disp) - вы─
годен при большом количестве таких же эк─
зотических действий с переменной, как упо─
мянутое вычитание.Тем более, IY часто про─
стаивает. Понятно, что вариант с IY будет
короче, чем LD HL,var1:SUB (HL), но длин─
нее LD HL,var1:LD A,(HL):XOR 1:LD (HL),A.
Подпрограмма с возможностью затыкания
входа реализуется подставлением или убира─
нием команды RET в её начале, без каких бы
то ни было переменных.
Часто можно посчитать сплошной кусок
программы состоящим из нескольких последо─
вательных действий. Обычно бессознательно
мы пишем эти секции независимыми от резу─
льтатов друг друга. Это важно для "быстро─
го" (или "экстремального") программирова─
ния (термин для программирования без опти─
мизации и поиска ошибок - когда на это нет
времени,нужно гарантировать их изначальное
отсутствие). Но противопоказано для опти─
мизации. Как правило,после какого-либо ци─
кла ряд регистров содержит нуль.Между тем,
нули могут потребоваться ниже (возможно,не
в других регистрах и не в соседних эта─
пах). Перестановкой регистров и этапов мо─
жно выигрывать значительные объёмы.
За счёт перестановки регистров можно
перенести большинство 16-разрядных опера─
ций в регистровую пару HL, освободить ин─
дексные регистры, получить дополнительный
операнд в стеке через EX (SP),HL, свести
операции память-память к команде LDI (LDD)
и т.п.
Обращайте внимание на команды инициали─
зации регистров и флагов.На всякий случай,
вписывая их, всегда выделяйте (например,
сдвигом строки текста влево).Это будет на─
поминанием,что при удачном стечении обсто─
ятельств команды можно закомментировать.
Одинаковые инициализации можно также про─
водить через PUSH...POP.
При оптимизации используют и свойства
циклов:они бывают разнонаправленные;бывают
со входом в середину,с выходом из середины
или со стандартным входом-выходом; параме─
трические и по условию; с прединкрементом
и постинкрементом (можно переносить часть
операций из конца цикла в начало и наобо─
рот).
Допустимо использовать фрагменты ПЗУ,
которые вам понравятся. В ПЗУ 48k реализо─
ваны многие полезные действия, не послед─
ними из которых являются LDDR : RET (адрес
5729=#1661 ) и LDIR : RET (адрес 13251=
=#33C3 ), идеальные для условных вызовов и
условных переходов. Но несовместимость,ко─
торая может получиться, если вы программи─
руете по ВАШЕ нестандартное ПЗУ, будет це─
ликом именно на ВАШЕЙ совести :). Базовый
вариант ПЗУ - 1982 года, далее происходили
дополнения (следует, впрочем,отличать 1982
от SOS 128k, тоже помеченного этой датой).
С точки зрения памяти, часто бывает вы─
годно хранить упакованными отдельные редко
используемые части программы - пусть не
код, а описание, или часть шрифтов, карт и
т.п.При последующем "склеивании" программы
есть резон не паковать уже сжатые таким
образом части.
Оптимизация по сжатому размеру
Во многих случаях важен не объём памя─
ти, занимаемый программой, а её размер на
диске или в ПЗУ (будь она прошивкой). Гра─
мотное использование упаковщиков позволяет
решить многие аспекты этой проблемы.
Большинство применяемых для сжатия кода
упаковщиков основано на методах LZ (A.
Lempel, J.Ziv) - копирования повторяющихся
участков из уже распакованной половины
файла в ещё не распакованную. Желательно
знать и точный формат упакованных данных,
но и основных представлений достаточно.
Важную роль в определении физического
размера программы будет играть загрузчик и
детали,ему сопутствующие: распаковщик,блок
SETUP (настройки программы),процедуры ини─
циализации.Инициализация тоже вошла в этот
список,поскольку хранение её в памяти ПОС─
ЛЕ запуска программы чаще не обосновано -
наравне с другими частями загрузчика.Но не
всегда:распаковщик может использовать сама
программа для извлечения нужных её данных
из файлов, а загрузчиком (если он турбиро─
ванный) тоже не грех воспользоваться сно─
ва,без его повторения в рабочей части про─
граммы. Встречаются,конечно,и случаи,когда
программа должна иметь возможность сохра─
нять саму себя, и ей приходится держать в
памяти все свои части.
Для достижения наибольшей компрессии и
оптимального использования хвостов секто─
ров лучше всего иметь единственный упако─
ванный блок (если не один упакованный, то
по крайней мере загружаемый - один),с дан─
ными, разбрасываемыми впоследствии по раз─
ным частям памяти. Число перебросок лучше
продумать и сократить ещё на этапе разра─
ботки программы. Впрочем, столкнувшись с
этим, нетрудно лишний раз исправить прог─
рамму с точки зрения распределения памяти,
изменив пару констант.
При компрессии музыкальных и графичес─
ких данных нужно выбирать наиболее подхо─
дящие форматы и компрессоры.Однако при ма─
лом количестве таких данных общий компрес─
сор/декомпрессор выгоднее.
Оптимизация кода для сжатия являет со─
бой особую проблему. Здесь помогает только
метод проб и ошибок. Но, в частности, оче─
видно, что:
- похожие блоки программы следует разме─
щать рядом;
- некоторые циклы выгодно раскрывать в
конструкцию DUP...EDUP ;
- серия JP на один адрес пакуется лучше,
чем серия таких же JR ;
- все области с необязательным содержи─
мым (переменные) лучше заполнить нулями
или кодами соседних команд;
- области буферов лучше исключать из ко─
дового блока.
Для экспериментов с упаковкой проще
всего отассемблировать и выгрузить неско─
лько вариантов программы,после чего пооче─
рёдно сжимать их компрессором(ами).
Оптимизация по скорости
Перед проведением оптимизации по скоро─
сти не мешает провести оптимизацию и по
размеру - с целью удаления паразитных кус─
ков кода,но избегая добавления лишних вло─
женностей CALL...RET.
В дальнейшем оптимизируемая по скорости
программа может как уменьшаться по длине,
так и увеличиваться.
Изначально оптимизации подвергаются ал─
горитмы. Многие вещи можно производить ты─
сячей способов: сортировку, умножение,про─
крутку экрана,спектральные преобразования,
декодирование Хаффмана, геометрические по─
строения, чтение байтов с диска и т.д. Тот
способ, который может быть менее оправдан
с точки зрения размера и простоты реализа─
ции, порой оказывается более быстрым.
Например,умножение через "квадрат полу─
суммы минус квадрат полуразности" требует
предварительной генерации таблицы квадра─
тов и,соответственно,выделения под неё па─
мяти, быстрая сортировка может быть вообще
не наглядной, а быстрое декодирование Хаф─
фмана - не только не наглядным, но ещё и
труднопроверяемым. Но тем не менее,они вы─
годны по скорости.
Разумеется,всё ускорять не надо,тем бо─
лее варварскими методами табличного вида,
заполняющими всю память. Нужно выбирать
именно те части программы, которые влияют
на время её выполнения особенно сильно. В
простейших случаях такая часть одна: она
называется "внутренний цикл" (inner loop)
и находится легко - в месте самой глубокой
вложенности многоярусного цикла.Как прави─
ло, циклы, число проходов которых исчисля─
ется тысячами, а то и миллионами, сводят к
табличным операциям и раскрывают в первую
очередь.(Раскрытие цикла заключается в за─
мене его повторами типа DUP...EDUP.) Кроме
того,продумывают всевозможные варианты за─
писи с разными регистрами и в каждом слу─
чае считают такты.
Может, однако,оказаться,что цикл второй
вложенности (если считать изнутри) содер─
жит так много команд, что замедляет прог─
рамму сильнее внутреннего. Между тем,о нём
часто забывают и потому не учитывают.
Более сложный вариант - программа со
множеством поочерёдно проводимых операций.
Если операции заключены в серию циклов,
затруднительно даже выяснить, сколь долго
в сумме выполняется каждая из них. Подсчи─
тать число проходов можно,поставив в любом
месте:
PUSH AF
LD A,0
ADD A,1
LD ($-3),A
LD A,0
ADC A,0
LD ($-3),A
LD A,0
ADC A,0
LD ($-3),A
(и т.д.)
POP AF
Если найдётся способ оценить среднее
количество тактов в каждой операции,то,уз─
нав количество проходов, мы получим и вре─
мя. Можно попытаться подсчитывать и преры─
вания.Можно исключать процедуры по одной и
сравнивать общее время.
В зависимости от вклада, вносимого каж─
дой процедурой,можно выбрать две-три-деся─
ток самых неторопливых и заняться ускоре─
нием именно их.Ускорение заключается в вы─
боре разных реализаций простейших операций
и удобного для них разделения регистров
процессора. Желательно содержать все обра─
батываемые данные, сколько возможно, в ре─
гистрах - с минимальным использованием па─
мяти.Используйте альтернативные наборы. IX
и IY здесь лучше к применению в роли сум─
маторов или счётчиков цикла, поскольку об─
ращение через них к памяти дольше обычно─
го. (Опять-таки,бывают и обратные случаи.)
Будьте также осторожны с IY - при режиме
прерываний IM 1 или вызовах RST 56 модифи─
цировать его опасно.
Данные удобно размещать по круглым ад─
ресам, что упростит расчёт этих адресов, а
заодно сможет позволить заменять что-ни─
будь вроде INC HL на, скажем, INC L.
После внедрения семейства таблиц можно
перейти к рассмотрению возможностей ОБХО─
ДОВ процедур в каких-либо случаях, с соот─
ветствующей подменой результата. В конце
концов запретите прерывания и задействуйте
стек - либо как хранилище потока данных на
ввод или вывод,либо как дополнительное 16-
разрядное слагаемое при вычислениях.
Если показалось, что близок тупик,ищите
другой алгоритм. Как правило,он находится.
Больше заранее просчитанных данных;исполь─
зование особенностей стека ( LD SP,HL, ав─
тоинкремент,таблицы процедур,цепочки вызо─
вов в стеке); другие форматы таблиц,умень─
шающие количество упоминаемых констант;для
визуальных эффектов - огрубление вычисле─
ний,поблочный вывод...и вообще ЛЮБЫЕ дикие
идеи, какие только придут в вашу голову.
Далеко не все варианты испробованы, велик
шанс найти что-то новое, ни с кем не сове─
туясь!
Процедуры ПЗУ
для использования в программах
Добавляю несколько интересных процедур
ПЗУ к списку уже известных 8880,8020,3584
и т.д.
Некоторые из этих процедур очень корот─
кие,и можно задаться вопросом:зачем их ис─
пользовать?Однако даже такие очень полезны
для оставления в качестве адресов на стеке
- этакий Форт. Список публикуется впервые.
#e88:
;HL=экранный адрес с прибавлением #800
LD A,H
RRCA*3
DEC A
OR #50
LD H,A
EX DE,HL ;DE=атрибутный адрес
LD H,C,L,B ;B=например, число
;атрибутных строк,
;C=например, 0
ADD HL,HL*5
LD B,H,C,L ;BC=HL=CB*32
RET
#e9b:
LD A,24
SUB B
#e9e:
LD D,A
RRCA*3
AND #E0
LD L,A
LD A,D
AND #18
OR #40
LD H,A ;HL=адрес A-й знакоместной
;строки
RET
#1117:
INC HL
LD (HL),E
INC HL
LD (HL),D
SCF
RET
#1660:
EX DE,HL
LDDR
RET
#169a:
LD D,(HL)
INC HL
LD E,(HL)
RET
#16fd:
LD (HL),C
INC HL
LD (HL),B
RET
#172d:
LD C,A
LD B,0
ADD HL,BC
LD C,(HL)
INC HL
LD B,(HL)
DEC HL
RET
#18b9:
INC HL*6
LD A,(HL)
RET
#19f5:
ADD HL,DE
PUSH DE
LDIR
POP HL
RET
#1bb0:
RST 8
DB #FF ;"0 OK..."
#1c19:
LD C,(HL)
INC HL
LD B,(HL)
EX DE,HL
PUSH BC
RET
#1e7d:
OUT (C),A
RET
#229b:
устанавливает цвет бордюра и 23624 по A
#231a:
LD C,1
RET Z
LD C,-1
RET
#2aee:
EX DE,HL
INC HL
LD E,(HL)
INC HL
LD D,(HL)
RET
#2ba6:
EX DE,HL
LD A,B
OR C
RET Z
PUSH DE
LDIR
POP HL
RET
#2f8b:
PUSH DE
LD L,A,H,0
LD E,L,D,H
ADD HL,HL
ADD HL,HL
ADD HL,DE
ADD HL,HL ;HL=A*10
LD E,C
ADD HL,DE ;HL=A*10+C
LD C,H,A,L
POP DE
RET
#3406:
A=BC=A*5
ADD HL,BC
RET
#350b:
PUSH HL
LD A,0
LD (HL),A
INC HL
LD (HL),A
INC HL
RLA
LD (HL),A
RRA
INC HL
LD (HL),A
INC HL
LD (HL),A
POP HL
RET
A. Coder