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

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

ZXNet: 500:95/462.53
E-mail: bestview@mtu-net.ru
WWW: http://www.ivr.da.ru

Улучшение сжатия программ на ассемблере Z80

Радиомир. Ваш компьютер» 4/2003)

Наиболее часто используемый для сжатия ассемблерных программ упаковщик HRUST Дмитрия Пьянкова, как я выяснил в результате экспериментов, сжимает даже два-три стоящих рядом одинаковых байта. Здесь я хочу рассказать о том, как это можно использовать для улучшения сжатия программ. Улучшение сжатия сэкономит занимаемое программой место на диске, а если программа написана так, что её отдельные части хранятся в памяти в упакованном виде и распаковываются при необходимости, то это поможет и сэкономить место в памяти.

Итак, в чём сущность способа? Часто используется такой приём оптимизации, когда переменная размещается непосредственно в команде загрузки, тем самым сокращается объём программы и уменьшается время её выполнения (более подробно об этом вы можете прочитать в [1]).

Например, вместо такого:

  VAR  DS  1
       .........
       LD  A,(VAR)

можно записать так (будет на 2 байта короче и на 6 тактов быстрее):

       LD  A,0
  VAR  EQU $-1

Если первоначальное значение переменной не используется в программе, то имеет смысл сделать это значение таким, чтобы после компиляции получилась последовательность одинаковых байтов, которая может быть сжата упаковщиком. То есть байт операнда должен равняться предыдущему байту. В данном случае предыдущий байт — код операции «LD A,n», равный #3E, следовательно, пишем так:

       LD  A,#3E
  VAR  EQU $-1

В результате после компиляции соответствующий участок программы будет представлять собой два одинаковых байта #3E.

Такой способ можно применять и для других команд, в которых первоначальное значение операнда не используется. Пусть, например, в программе есть команда CALL 0, где вместо 0 перед выполнением этой команды записывается какой-то адрес. Точно так же, для повышения степени сжатия делаем операнд равным не 0, а числу, состоящему из байтов, равных предыдущему байту (коду операции CALL nn), в результате получаем CALL #CDCD.

Если в программе с помощью директив ассемблера DB, DW, DS определены переменные или участки памяти, первоначальное значение которых не используется, то для улучшения сжатия их тоже имеет смысл заполнять значением предыдущего байта. Пусть, например, в программе имеется такой фрагмент:

       JP  label1
       DS  20

Команда JP в машинном коде представляется тремя байтами; последний из них — старший байт операнда, то есть label1/256. Соответственно, в программе пишем:

       JP  label1
       DS  20,label1/256

Итак, что мы имеем? При использовании описанного способа улучшения сжатия программисту приходится помнить коды операций (или каждый раз обращаться к справочнику, или набирать команду в отладчике и смотреть, какой получится код). К тому же, чтобы не запутаться, для каждой команды, в которой производится замена операнда, придётся писать комментарий, поясняющий, что указанное в команде значение нужно лишь для лучшего сжатия и не используется при работе программы.

Понятно, что это весьма неудобно. Поэтому я предлагаю реализовать поддержку этого способа улучшения сжатия на уровне компилятора. Можно использовать следующую форму записи: если в команде вместо значения операнда поставить символ «\», то компилятор заполнит байт (байты) операнда значением предыдущего байта памяти. Так, запись «LD A,\» будет эквивалентна «LD A,#3E», «CALL \» — эквивалентна «CALL #CDCD», «JR \» — «JR #18», и так далее. Команды «DB \», «DW \», «DS n,\» будут соответственно означать, что 1, 2, n байтов памяти заполняются значением предыдущего байта.

При этом программисту не надо будет помнить коды команд и комментировать такие участки программы; без всяких комментариев будет ясно, что такая команда используется для повышения степени сжатия программы.

Но пока таких компиляторов нет, и заменять операнды придётся вручную. Для удобства я приведу сведения о том, на что надо заменять операнд, для некоторых команд ассемблера.

LD A,\  = LD A,#3E         LD IX,\   = LD IX,#2121
LD B,\  = LD B,#06         LD IY,\   = LD IY,#2121
LD C,\  = LD C,#0E         LD SP,\   = LD SP,#3131
LD D,\  = LD D,#16         LD (HL),\ = LD (HL),#36
LD E,\  = LD E,#1E         ADD A,\   = ADD A,#C6
LD H,\  = LD H,#26         ADC A,\   = ADC A,#CE
LD L,\  = LD L,#2E         SUB \     = SUB #D6
LD XH,\ = LD XH,#26        SBC A,\   = SBC A,#DE
LD XL,\ = LD XL,#2E        AND \     = AND #E6
LD YH,\ = LD YH,#26        OR \      = OR #F6
LD YL,\ = LD YL,#2E        XOR \     = XOR #EE
LD BC,\ = LD BC,#0101      JP \      = JP #C3C3
LD DE,\ = LD DE,#1111      JR \      = JR #18
LD HL,\ = LD HL,#2121      CALL \    = CALL #CDCD

Кстати говоря, компилятор может действовать и более сложным образом, не обязательно заменяя байт (байты) операнда на значение предыдущего байта. Пусть в программе имеется следующая последовательность команд:

       LD   HL,#1234
       CALL subr1
       LD   HL,\
       CALL subr1

В этом случае выгоднее будет сделать операндом третьей команды не #2121, а #1234 — тогда после компиляции получим две идущие друг за другом одинаковые группы из 6 байтов, которые сожмутся более эффективно. Так вот, компилятор может отслеживать подобные ситуации и решать, какие значения будут принимать байты операнда в «\»-командах.

Ещё один вариант логики работы компилятора — не принимать самому решения о том, какими значениями заполнять байты операндов в «\»-командах, а переложить эту задачу непосредственно на упаковщик. При этом компилятор формирует два выходных файла: в одном — откомпилированная программа, а в другом указывается, какие байты этой программы могут принимать произвольные значения. Упаковщик анализирует оба файла и выбирает значения этих байтов так, чтобы размер программы после упаковки был минимальным.

Но такой упаковщик надо ещё написать! Или, если это покажется более удобным, можно написать отдельную программу, которая брала бы описанные выше два файла и выдавала один файл с установленными значениями байтов, который и подаётся затем на вход упаковщику. Но, чтобы обеспечить наилучшее сжатие, эта программа должна знать, как именно осуществляется упаковка.

Кстати, в [2] я писал о способе повышения сжатия музыкальных модулей, основанном на автоматическом определении байтов модуля, первоначальные значения которых не используются при проигрывании (а значит, могут быть любыми). За счёт заполнения этих байтов значением предыдущего байта и достигалось повышение степени сжатия. Так вот, там можно точно так же выполнять замену не просто на значение предыдущего байта, а так, чтобы сжатие было наилучшим.

Литература

  1. И.Рощин. «Оптимизация на примере intro „Start“». «Радиомир. Ваш компьютер» 7—10/2001.
  2. И.Рощин. «Повышение степени сжатия музыкальных модулей». «Радиомир. Ваш компьютер» 7/2002.

Другие мои статьи об усовершенствовании ассемблера:

Другие мои статьи о сжатии:

1. 

«Повышение степени сжатия музыкальных модулей». «Радиомир. Ваш компьютер» 7/2002.

2. 

«Улучшение сжатия данных: ещё одна идея». «Радиомир. Ваш компьютер» 8/2003.

3. 

«Сжатие данных: от битов к байтам». «Радиомир. Ваш компьютер» 11/2003.

Другие мои статьи о различных приёмах оптимизации при программировании на ассемблере 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». «Радиомир. Ваш компьютер» 6/2003.

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