© Иван Рощин, Москва
| ZXNet | : 500:95/462.53 |
| : bestview@mtu-net.ru | |
| WWW | : http://www.ivr.da.ru |
(«Радиомир. Ваш компьютер» 8/2005)
В этой статье описаны некоторые приёмы оптимизации при программировании на
ассемблере Z80. Возможно, не я первый их придумал (так как они достаточно
очевидны), но мне не приходилось
| 1. | Если флаг C=1, поместить в A константу n, иначе обнулить A. |
SBC A,A ;Если флаг C=1, то получим A=#FF, иначе A=0.
AND n ;#FF превратится в n, а 0 не изменится.
| 2. | Если флаг C=1, поместить в A константу n1, иначе константу n2. |
SBC A,A ;Если флаг C=1, то получим A=#FF, иначе A=0.
AND n1-n2 ;#FF превратится в n1-n2, а 0 не изменится.
ADD A,n2 ;Получим соответственно n1 или n2.
| 3. | Пусть имеется подпрограмма, в конце которой два одинаковых фрагмента, длина каждого x байтов (x>=4). Вот пример: |
...........
LD A,(HL)
INC HL
LD H,(HL)
LD L,A
LD A,(HL)
INC HL
LD H,(HL)
LD L,A
RET
Оптимизация с помощью организации цикла (если поставить перед повторяющимся
фрагментом команду LD B,2, а после фрагмента команду DJNZ M1, где
M1 адрес первой команды фрагмента) даст выигрыш в x4
байта (а если значение регистра B нельзя изменять, то придётся организовывать
цикл
........
LD B,2
M1 LD A,(HL)
INC HL
LD H,(HL)
LD L,A
DJNZ M1
RET
Но есть более элегантный способ оптимизации, обеспечивающий выигрыш в
x3 байта (т.е. на байт больше): просто ставим перед началом
повторяющегося фрагмента
........
CALL $+3
LD A,(HL)
INC HL
LD H,(HL)
LD L,A
RET
Напомню, $ это адрес текущей команды. Так как длина команды CALL 3 байта, то $+3 будет адресом следующей команды. Таким образом, команда CALL $+3 передаст управление на первую команду повторяющегося фрагмента, причём в стек будет занесён адрес этой команды. После того, как фрагмент выполнится в первый раз, по команде RET управление будет передано на сохранённый в стеке адрес начала этого фрагмента. Таким образом, фрагмент выполнится во второй раз, что и требуется. Затем при выполнении команды RET произойдёт уже выход из подпрограммы.
Нетрудно догадаться, что если надо выполнить повторяющийся фрагмент четыре раза, то для этого достаточно поставить перед его началом две команды CALL $+3; если надо выполнить восемь раз, то три таких команды, и т.д. Но тут уже может быть выгоднее оптимизация с помощью организации цикла.
И ещё. Если в подпрограмме перед повторяющимся фрагментом есть другие команды и можно выбирать адрес компиляции этой подпрограммы, то, если повезёт, возможна дополнительная оптимизация за счёт того, что адрес перехода в команде CALL (или старший байт этого адреса) будет одновременно являться кодами команд (или кодом команды), расположенных перед повторяющимся фрагментом. Выигрыш может составить один или два байта.
Объясню на примерах (за основу возьмём уже рассмотренный ранее пример). Сначала рассмотрим случай, когда выигрыш равен двум байтам. Это возможно, если перед повторяющимся фрагментом стоит либо двухбайтовая команда, либо две однобайтовых. Для примера, пусть это будут две однобайтовых команды: LD H,B (код #60) и LD L,C (код #69). Тогда, подобрав адрес компиляции подпрограммы, можно сделать так:
........
#695F: DB #CD ;Код команды CALL nn.
#6960: LD H,B ;Эти две команды, кроме прямого их
LD L,C ;назначения, образуют адрес вызываемой
;подпрограммы - #6960 (сначала младший байт
;адреса, потом старший).
#6962: LD A,(HL)
INC HL
LD H,(HL)
LD L,A
RET
При выполнении находящейся по адресу #695F команды CALL в стек будет помещён адрес, на 3 байта больший (т.е. #6962), и произойдёт переход к адресу #6960. Выполнятся команды LD H,B и LD L,C, а затем в первый раз выполнится повторяющийся фрагмент. После выполнения команды RET произойдёт переход к ранее запомненному в стеке адресу #6962, повторяющийся фрагмент выполнится во второй раз, а затем после второго выполнения команды RET произойдёт выход из подпрограммы.
Кстати, если бы адрес #6960 по
Теперь рассмотрим другой случай. Предположим, что перед началом фрагмента стоят трёхбайтовая команда LD BC,#1234 и однобайтовая команда EXX (код #D9). Сэкономить два байта тут уже не получится, но один байт всё же можно, если выбрать адрес компиляции так, чтобы в нижеприведённом фрагменте команда EXX располагалась по адресу вида #D9xx.
........
LD BC,#1234
DB #CD ;Код команды CALL nn.
DB #xx ;Младший байт адреса.
#D9xx: EXX ;Старший байт адреса.
LD A,(HL)
INC HL
LD H,(HL)
LD L,A
RET
| 4. | Пусть в памяти по адресу nn хранится однобайтовая переменная, равная 0 или 1, и надо изменить её так, чтобы вместо 0 стало 1 и наоборот. Тогда вместо такого: |
LD HL,nn ;3 байта, 10 тактов.
LD A,(HL) ;1 байт, 7 тактов.
XOR 1 ;2 байта, 7 тактов.
LD (HL),A ;1 байт, 7 тактов.
;Итого: 7 байтов, 31 такт.
можно сделать так:
LD HL,nn ;3 байта, 10 тактов.
INC (HL) ;1 байт, 11 тактов.
RES 1,(HL) ;2 байта, 15 тактов.
;Итого: 6 байтов, 36 тактов.
и получится на байт короче (но на 5 тактов медленнее!). Также в этом случае не изменяется аккумулятор.
Аналогично, если изменяемая переменная находится в одном из регистров B, C, D, E, H, L (обозначим его r), то вместо такого:
LD A,r ;1 байт, 4 такта.
XOR 1 ;2 байта, 7 тактов.
LD r,A ;1 байт, 4 такта.
;Итого: 4 байта, 15 тактов.
выгоднее так:
INC r ;1 байт, 4 такта.
RES 1,r ;2 байта, 8 тактов.
;Итого: 3 байта, 12 тактов.
Это и короче на байт, и быстрее на 3 такта, и не изменяется аккумулятор.
И ещё один приём оптимизации. Додумался до него сам, но потом стал
проверять, не встречаются ли примеры его использования ещё у
| 5. | Сложить A с содержимым регистровой пары BC, DE или HL (обозначим её r1r2) и поместить результат в ту же или другую из этих регистровых пар (обозначим её r3r4). |
ADD A,r2 ;Получили мл. байт суммы и признак переноса.
LD r4,A ;Сохранили мл. байт суммы.
ADC A,r1 ;A=мл. байт суммы + признак переноса + r1.
SUB r4 ;A=признак переноса + r1 = ст. байт суммы.
LD r3,A ;Сохранили ст. байт суммы.
Если результат надо получить в HL, обычно делают так (пример для случая, когда второе слагаемое в BC):
LD H,0 ;2 байта, 7 тактов.
LD L,A ;1 байт, 4 такта.
ADD HL,BC ;1 байт, 11 тактов.
;Итого: 4 байта, 22 такта.
Это на байт короче. Но зато описанный вначале способ на два такта быстрее! Смотрите:
ADD A,C ;1 байт, 4 такта.
LD L,A ;1 байт, 4 такта.
ADC A,B ;1 байт, 4 такта.
SUB L ;1 байт, 4 такта.
LD H,A ;1 байт, 4 такта.
;Итого: 5 байтов, 20 тактов.
А ведь иногда требуется максимальное быстродействие даже за счёт увеличения длины программы.
Замечу, что с точки зрения влияния на флаги эти два способа не эквивалентны.
|
Другие мои статьи о различных приёмах оптимизации при программировании на ассемблере Z80:
|