Методы оптимизации программного кода
для 8-битовых процессоров.

В настоящее время, в связи с появлением компьютеров нового поколения, программисты всё меньше внимания стали уделять компактности и быстродействию разрабатываемого программного обеспечения. Однако ещё совсем недавно, всего 10 лет назад дело обстояло совсем не так, как сейчас. Тогда в нашей стране компьютеры только начинали появляться. Это были не современные 16 и 32-битовые платформы, к которым мы уже успели так быстро привыкнуть, а это были в основном компьютеры, оснащённые 8-битовыми процессорами (с тактовой частотой порядка 1 Мгц), 64 Кб оперативной памяти, дисководами для дискет 360 Кб и так далее. Хорошим примером такого компьютера служит модель Пълдин-601/Пълдин-601A. Эти компьютеры в своё время были очень распространены в нашей стране и в странах бывшего СССP. Они были оснащены процессором СМ601 (аналог MC6800) и вся приводимая в статье информация будет приведена именно для них.

Компактность программ и их быстродействие выходили на первый план в те времена, когда были широко распространены Пълдины и подобные им компьютеры. Конечно, в настоящее время такие компьютеры если и остались, то для них уже никто не разрабатывает нового программного обеспечения. Поэтому, всё что приведено в этой статье может быть лишь хорошим путеводителем в историю, но может послужить и советом для современных начинающих программистов - ведь начинать всегда нужно с чего-то простого (т.е. с 8-битового ассемблера), а уже потом переходить к сложному (т.е. к 16 и 32-битовому ассемблеру).

Существует большое количество методов и приёмов оптимизации программного кода. Далее будут приведены лишь самые простые, но в то же время и самые наглядные примеры.

1) Использование инструкций bra и bsr вместо jmp и jsr принесёт выигрыш в 1 байт, так как bra и bsr - инструкции с относительной адресацией, т.е. двухбайтовые. Однако применять их можно только в том случае, если переход нужно совершить в пределах ближайших 255 байт (примерно по 127 байт в каждую сторону от данного места). Однако часто бывают случаи, что в какой-то области, размером около 255 байт, сконцентрировано очень много ОДИНАКОВЫХ переходов к одной и той же дальней метке (т.е. jmp). Тогда следует где-то приблизительно в середине этой области применить переход jmp, а во всех остальных местах - jmp заменить переходом bra на эту метку (т.е на этот jmp).

2) Инструкции типа ldaa #0 следует заменить на clra. А вместо конструкций типа

        clra
        staa        per1

применять clr per1.

3) 1 байт можно выиграть, заменив конструкцию типа

        ldx        #per1
        clr        $00,x
        clr        $01,x

на

        ldx        #$0000
        stx        per1

4) Если требуется разделить или умножить 1-байтовое число на 2, 4, 8, 16 и так далее, то не обязательно прибегать к громоздким конструкциям с вызовом системных функций, предназначенных для этого. Следует воспользоваться общеизвестным фактом, что при сдвиге битов влево число умножается на 2, а при сдвиге вправо - делится на 2 и пользоваться инструкциями типа asra и asla.

5) Следует всегда продумывать, какой условный переход применить, чтобы избежать лишних безусловных переходов. Например, следующие две конструкции выполняют абсолютно одинаковую функцию, но вторая короче первой на 2 байта.

Первый вариант:

             cmpa       #12
             bne          per1
             bra           per2
per1       ...
             rts
per2       ...
             rts

Второй вариант:

             cmpa      #12
             beq        per2
             ...
             rts
per2      ...
             rts

6) Следующий хитрый приём даёт выигрыш 1 байт. Две следующие последовательности выполняют абсолютно одинаковую функцию.

Первый вариант:

set_A       staa        $06
               bra          per1
set_B       stab        $05
per1        ...

Второй вариант:

set_A        staa        $06
                DB          $8c         ; CPX #
set_B        stab        $05
                ...

В первом случае выполняется явный обход инструкции stab $05. А во втором - при переходе к метке set_A и выполнении инструкции staa $06, следующие 3 байта воспринимаются как CPX #$d705 и не выполняют ни полезных, ни вредных в данном случае, действий.

7) Из конструкции

        ldaa         per1
        tsta
        bne          m2

можно абсолютно спокойно убрать инструкцию tsta. Всё будет работать абсолютно также, ведь ldaa и без того сама установит флаг нуля, если переменная per1 равна нулю.

8) Если необходимо сравнить какую-то переменную с числом 255, то вместо конструкции типа

        ldaa        per2
        cmpa       #$ff
        beq         m2

логично будет применить такой набор команд:

        ldaa        per2
        inca
        beq         m2

Но здесь нужно быть внимательным, так как здесь, в отличие от приведённой выше конструкции с использованием cmpa, значение регистра "a" увеличивается на единицу. Но если далее это значение не используется, то этот метод вполне применим. Аналогично, можно применить deca вместо cmpa #$01.

9) Если требуется ненадолго запомнить какое-либо значение, то можно вместо того, чтобы выделять для этих целей переменную, воспользоваться стеком. Например, вместо

        staa        per1
        ...
        ldaa       per1

можно воспользоваться

        psha
        ...
        pula

Здесь выигрыш будет 5 байт (5ый байт за счёт того, что не нужно будет описывать переменную). Однако здесь следует быть очень внимательными и следить за тем, чтобы между psha и pula не изменялся стековый указатель (а это происходит при выполнении таких инструкций как txs, rts, bsr, jsr, lds). Недопустимо, чтобы psha был в основной программе, а pula в подпрограмме или наоборот. Также недопустимо, чтобы между "нашими" psha и pula число других psha/pshb было не равно числу других pula/pulb. В следующем примере как раз применяется этот приём.

10) Далее будет приведён классический пример процедуры, которая выводит на экран содержимое регистра "a" в шестнадцатеричном виде. На компьютере Пълдин-601 этот алгоритм применён в прерывании int $25, выполняющем соответствующие действия. И на этом примере будет показан ещё один метод оптимизации программного кода.

               psha
               lsra
               lsra
               lsra
               lsra
               anda       #$0f
               oraa       #$30
               cmpa      #$3a
               bcs         prhexz1
               adda       #7
prhexz1  int           $22
               pula
               anda       #$0f
               oraa       #$30
               cmpa     #$3a
               bcs        prhexz2
               adda      #7
prhexz2   int         $22
                rts

Применим метод, очень похожий на рекурсию, но в данном случае подпрограмма вызывает не саму себя, а лишь свою часть и то в качестве "другой" подпрограммы. Инструкция rts, следующая последней, в первом случае служит для выхода из подпрограммы, вызываемой командой bsr, а во втором случае, из подпрограммы вообще. Посмотрим, насколько теперь сокращается размер этой подпрограммы.

               psha
               lsra
               lsra
               lsra
               lsra
               bsr         prhex
               pula
prhex      anda       #$0f
               oraa       #$30
               cmpa      #$3a
               bcs         prhexz1
               adda       #7
prhexz1  int          $22
               rts

Однако и это не предел. Оказывается, можно сократить размер этой подпрограммы ещё на 2 байта, изменив арифметику. В следующем примере используется инструкция десятичной коррекции сложения daa, которую программисты практически нигде не применяют на практике.

               psha
               lsra
               lsra
               lsra
               lsra
               bsr          prhex
               pula
               anda        #$0f
prhex      adda       #$90
               daa
               adca       #$40
               daa
               int          $22
               rts


11) Наконец, следует разобраться, почему именно адреса $0000-$00ff сделаны рабочей областью BIOS, UniDOS и страничного ROM на компьютере Пълдин-601. Это сделано для того, чтобы уменьшить размер программного кода. Ведь основной блок BIOS должен уместиться в 4096 байт, а каждая страница памяти - в 8192 байт. Это достигается за счёт того, что инструкции типа ldaa, stx, cmpb и прочие при обращении к адресам $0000-$00ff кроме Extended-адресации (команда при этом имеет длину 3 байта) могут использовать Direct-адресацию (команда при этом имеет длину 2 байта). Очевидно, чтобы обратиться к адресам $0100-$ffff нужна обязательно трёхбайтовая инструкция. Вот поэтому адресное пространство $0000-$00ff и выделено для того, чтобы там размещали свои переменные BIOS, UniDOS и страничный ROM. В табл. 1 приводится сравнение для некоторых коротких подпрограмм, зашитых в BIOS компьютера Пълдин-601.

Табл. 1.

Функция

Длина кода

Длина кода, если бы применялась Extended адресация

Проигрыш

int $2e

38

46

8

int $2f

56

68

12

int $37

36

40

4

int $2d

94

124

30

Суммарно

224

278

54

Уже в этих четырёх коротких подпрограммах, обслуживающих соответствующие прерывания, проигрыш составил бы 54 байта. А каков был бы проигрыш, если подобный расчёт сделать для всего BIOS! Тогда бы он абсолютно точно не уместился в 4096 байт, а страничный ROM - в 8192 байта. Например, в странице UniBIOS нет ни одного "свободного байта" - по адресу $d7fe - rts последней подпрограммы, в $d7ff - байт, переводящий контрольную сумму в ноль, а с адреса $d800 (и до конца, т.е. до $dfff) расположены шрифты 8x8 (2048 байт).


< < < На главную страничку < < <