Страница Ивана Рощина > Статьи >

© Иван Рощин, Москва

ZXNet: 500:95/462.53
E-mail: 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 — адрес первой команды фрагмента) даст выигрыш в x–4 байта (а если значение регистра B нельзя изменять, то придётся организовывать цикл как-то иначе, что может ещё уменьшить выигрыш). Для вышеприведённого примера, где x=4, никакого выигрыша не получается:

        ........
        LD   B,2
M1      LD   A,(HL)
        INC  HL
        LD   H,(HL)
        LD   L,A
        DJNZ M1
        RET

Но есть более элегантный способ оптимизации, обеспечивающий выигрыш в x–3 байта (т.е. на байт больше): просто ставим перед началом повторяющегося фрагмента 3-байтовую команду CALL $+3:

        ........
        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 L,C, а за ней — LD H,B. Тогда они образовали бы адрес #6069. Или можно было бы заменить эти две команды на другие две: PUSH BC (код #C5) и POP HL (код #E1), и они образовали бы адрес #E1C5.

Теперь рассмотрим другой случай. Предположим, что перед началом фрагмента стоят трёхбайтовая команда 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:

1. 

«По поводу релоцируемых программ». «ZX-Ревю» 5—6/1997.

2. 

«Z80: оптимизация загрузки констант в регистры». «Радиолюбитель. Ваш компьютер» 9/2000, 2/2001 (под псевдонимом BV_Creator).

3. 

«Ещё о программировании арифметических операций». «Радиолюбитель. Ваш компьютер» 12/2000, 1—4/2001.

4. 

«Влияние команды OUTD на флаг переноса». «Радиолюбитель. Ваш компьютер» 5/2001, «Радиомир. Ваш компьютер» 8/2003 (под псевдонимом BV_Creator).

5. 

«Оптимизация на примере intro „Start“». «Радиомир. Ваш компьютер» 7—10/2001.

6. 

«Менеджер вызова подпрограмм из различных банков памяти». «Радиомир. Ваш компьютер» 12/2001, 2/2002, 4/2002.

7. 

«Улучшение сжатия программ на ассемблере Z80». «Радиомир. Ваш компьютер» 4/2003.

8. 

«Процедура сравнения строк на ассемблере Z80». «Радиомир. Ваш компьютер» 6/2003.

Страница Ивана Рощина > Статьи >