Start | Super Packer | Atari Graphics Studio | Graph2Font | Mads | MadPascal | Atari Zines | YouTube http://madteam.atari8.info  
MEMBERS
PRODUCTIONS
S C E N E
G A M E S
T O O L S
V B X E
GRAPHICS
WORK IN PROGRESS
Bubble Shooter
HARDWARE
Snes2Joy / Pad4Aatari / TOM rev2
Sio2SD / Pajero
Sio2SD / Rocky
Stereo / Pajero
GTIA / Psychol
ANTIC + VBXE test
ARTICLES / MAGAZINES
DEMO EFFECTS
LINKS
   

Bewesoft
Skróć to!
Syzygy #8

tłumaczył: Lizard/Aids

Często pisząc programy w asemblerze nie zastanawiamy się nad ich długością. (Kto przejmuje się tym, że edytor ma 24999B na tekst zamiast 25000...)

Czasami jednak długość kodu może być istotna! Czy potraficie wskazać takie sytuacje? Np. procedura dla Basica na szóstej stronie będzie miała 257 bajtów, albo rezydentny sterownik drukarki podniesie MemLo na $2001 i w związku z tym wasz ulubiony edytor odmówi współpracy, czy też procedura ładująca megademo osiągnie długość $181 bajtów, a mamy tylko 3 boot sektory na dyskietce w podwójnej gęstości... To są właśnie te sytuacje, kiedy musimy sobie powiedzieć: "SKRÓĆ TO!!!"

Takich momentów jest znacznie więcej. Osobiście zetknąłem się z tym problemem przy pisaniu BW-DOSa. W tym artykule przedstawię własne doświadczenia jakie nabrałem obniżając MemLo w BW-DOSie.

Listingi przedstawione w tekście są w formacie ATMAS II. W lewej kolumnie umieszczone zostały przykładowe fragmentu programu, a w prawej ich krótsze odpowiedniki.

O PROBLEMIE OGÓLNIE

Najlepsze rezultaty podczas skracania programów osiąga się poprzez zmianę algorytmu. Inaczej mówiąc, należy przyjżeć się podprogramom, które mogą być wyłączone i spróbować napisać część programu w inny, lepszy sposób.

W momencie, gdy tą metodą skrócimy co się da, a nadal będziemy potrzebować kilku bajtów, należy użyć sztuczek opisanych poniżej. Triki te pozwolą zyskać zaledwie kilka bajtów, lecz użyte wielokrotnie w programie dają dość pokażny zysk! Zabiegi skracające kod uczynią program mniej czytelnym, lecz kto by się tym przejmował, oraz w większości przypadków spowolnią program.

Przed zastosownaniem moich trików należy sprawdzić, czy dadzą jakikolwiek rezult. Nie zapominajcie też, że samomodyfikujący się kod nie będzie działać w ROMie!

STOSUJ PODPROGRAMY!

Najprostszą metodą na skrócenie programu jest stosowanie procedur tak często jak to możliwe. Gdy dwa kawałki procedury wyglądają tak samo, a różnią się tylko jednym szczegółem, to dobrze jest umieścić taki kawałek w podprogramie. Nie bójcie się używać krótkich podprogramów! Np.:

  LDA #1    |       LDA #1
  CLC       |       JSR ADDIT
  ADC INDEX |       ...
  STA INDEX |       LDA #20
  ...       |       JSR ADDIT
  LDA #20   |       ...
  CLC       | ADDIT CLC
  ADC INDEX |       ADC INDEX
  STA INDEX |       STA INDEX
  ...       |       RTS

 (7 bajtów) | (5 plus 6 bajtów)

Po czterech wywołaniach procedury ADDIT zyskujemy 2 bajty na każde następne wywołanie.

ZMIENNE

Umieszczajcie jak najwięcej zmiennych na stronie zerowej. Zyskacie po jednym bajcie przy każdorazowym odwołaniu do zmiennej i dodatkowo nie trzeba używać starszego bajtu do określenia adresu takiej zmiennej.

Gdy na stronie zerowej brakuje już miejsca (co się zdaża bardzo często) nadal można zyskać dwa bajty:

    LDA #0    |     LDA #0
    STA VAR   |     STA VAR+1
    ...       |     ...
    LDA VAR   | VAR LDA #0
    STA $D01A |     STA $D01A
    ...       |
VAR DTA B(0)  |

  (12 bajtów) | (10 bajtów)

Używając zmiennych jako flag można zaoszczędzić wiele miejsca używając tylko jednego bitu i obsługiwać ją poprzez rozkazy: LSR, ROR i BIT:

 * Zeruj flagę | * Zeruj flagę
        LDA #0 |    LSR FLAG
      LSR FLAG |
      STA FLAG |
* Ustaw flagę  | * Ustaw flagę
        LDA #1 |    SEC
     STA FLAG  |    ROR FLAG
* Testuj flagę | * Testuj flagę
      LDA FLAG |    BIT FLAG
     BNE LABEL |    BMI LABEL

   (12 bajtów) | (9 bajtów)

Zwróćcie uwagę, że pokazana metoda nie zmienia akumulatora, tak więc można zaoszczędzić jeszcze więcej miejsca usuwając rozkazy: PHA i PLA. Znacznik C będzie zmieniany po każdym ustawieniu/skasowaniu flagi.

SKOKI

Znając stan znaczników zastępujcie skoki bezwzględne (JMP) warunkowymi (B??). Oczywiście wtedy, gdy skok odbywa się gdzieś niedaleko:

      LDA TEXT,X   |       LDA TEXT,X
      ORA #$80     |       ORA #$80
      STA SCREEN,Y |       STA SCREEN,Y
      JMP LABEL    |       BMI LABEL
TEXT  DTA C'Text!' | TEXT DTA C'Text!'
LABEL ...          | LABEL ...

       (16 bajtów) | (15 bajtów)

Nigdy nie używaj rozkazu RTS za JSR:

 JSR LABEL | JMP LABEL
      RTS  |

 (4 bajty) | (3 bajty)

Gdy podprogram jest dostatecznie blisko, można zastosować skok warunkowy dla zyskania jednego bajtu, a jeśli to możliwe umieścić cały podprogram zamiast powyższego przykładu - zysk 4 bajtów!

Gdy adres skoku jest zbyt daleko by posłużyć się skokiem warunkowym (powyżej 127 bajtów), trzeba użyć skoku bezwzględnego, to jasne. Ale jeśli różnica pomiędzy adresem rozkazu skoku a adresem docelowym nie jest za duża dla dwóch skoków warunkowych (255 bajtów), można zaoszczędzić dodatkowo jeden bajt:

       CMP #1     |        CMP #1
       BNE LABEL1 |        BEQ LABEL3
       JMP LABEL2 |        ...
LABEL1 ...        |        RTS
       RTS        | LABEL3 BEQ LABEL2
LABELX ...        | LABELX ...
LABEL2 ...        | LABEL2 ...

       (8 bajtów) | (7 bajtów)

Wadą tej metody jest konieczność wystąpienia rozkazu RTS lub JMP pomiędzy skokami warunkowymi. Nie wszystko jednak stracone, gdy takiego rozkazu nie ma. Drugi skok należy wówczas wstawić w środek programu w miejscu, gdzie warunek zawsze będzie fałszywy:

       CMP #1     |        CMP #1
       BNE LABEL1 |        BEQ LABEL3
       JMP LABEL2 |        ...
LABEL1 ...        |        LDA #20
       LDA #20    | LABEL3 BEQ LABEL2
       STA VAR    |        STA VAR
       ...        |        ...
LABEL2 ...        | LABEL2 ...

      (11 bajtów) | (10 bajtów)

Do przeskoczenia pojedynczej instrukcji (tylko jedno lub dwubajtowej) nie używajcie skoków. Do tego można wykorzystać rozkaz BIT:

       CMP #1     |        CMP #1
       BNE LABEL1 |        BNE LABEL1
       INX        |        INX
       JMP LABEL2 |        DTA B($24)
LABEL1 DEX        | LABEL1 DEX
LABEL2 ...        |        ...

       (9 bajtów) | (7 bajtów)

PUT1   LDA #1     | PUT1   LDA #1
       BNE PUT    |        DTA B($2C)
PUT2   LDA #2     | PUT2   LDA #2
PUT    ...        | PUT    ...

       (6 bajtów) | (5 bajtów)

O co tu chodzi? Można powiedzieć, że pseudo rozkaz "DTA B($24)" powiadamia procesor by przeskoczył jeden bajt, a "DTA B($2C)" - dwa bajty. Tak naprawdę, procesor wykonuje w tym miejscu rozkaz BIT, którego operandem są przeskakiwane bajty. BIT zmienia tylko znaczniki N, V i Z.

OPERACJE NA SŁOWACH

Przy operacjach dodawania/odejmowania bajtu do/od słowa używajcie rozkazu INC/DEC do korekty starszego bajtu:

   LDA VAR   |       LDA VAR
   CLC       |       CLC
   ADC #20   |       ADC #20
   STA VAR   |       STA VAR
   LDA VAR+1 |       BCC LABEL
   ADC #0    |       INC VAR+1
   STA VAR+1 | LABEL ...

 (13 bajtów) | (11 bajtów)

   LDA VAR   |       LDA VAR
   SEC       |       SEC
   SBC #20   |       SBC #20
   STA VAR   |       STA VAR
   LDA VAR+1 |       BCS LABEL
   SBC #0    |       DEC VAR+1
   STA VAR+1 | LABEL ...

 (13 bajtów) | (11 bajtów)

Zwiększając/zmniejszając słowo o jeden również używajcie rozkazów INC/DEC:

   LDA VAR   |       INC VAR
   CLC       |       BNE LABEL
   ADC #1    |       INC VAR+1
   STA VAR   | LABEL ...
   LDA VAR+1 |
   ADC #0    |
   STA VAR+1 |

 (13 bajtów) | (6 bajtów)

   LDA VAR   |       LDA VAR
   SEC       |       BNE LABEL
   SBC #1    |       DEC VAR+1
   STA VAR   | LABEL DEC VAR
   LDA VAR+1 |
   SBC #0    |
   STA VAR+1 |

 (13 bajtów) | (8 bajtów)

Porównując słowa można stosować te same metody co przy odejmowaniu:

     LDA VAR1+1 |      LDA VAR1
     CMP VAR2+1 |      CMP VAR2
     BCC LOW    |      LDA VAR1+1
     BNE HIGH   |      SBC VAR2+1
     LDA VAR1   |      BCC LOW
     CMP VAR2   | HIGH ...
     BCC LOW    |
HIGH ...        |

    (14 bajtów) | (10 bajtów)

Można używać tego sposobu używając tylko znacznika C (mniejszy niż lub większy równy - BCC, BCS).

STAŁE

Ustawiając więcej niż jedną stałą można próbować używać jednobajtowych rozkazów INX, DEX, LSR, itp.:

    LDA #0   | LDX #0
    STA VAR1 | STX VAR1
    LDA #1   | INX
    STA VAR2 | STX VAR2

  (8 bajtów) | (7 bajtów)

    LDA #1   | LDA #1
    STA $30A | STA $30A
    LDA #0   | LSR
    STA $30B | STA $30B
    STA $309 | STA $309
    LDA #$80 | ROR
    STA $308 | STA $308

 (18 bajtów) | (16 bajtów)

Można też użyć wartości kończącej pętle:

     LDX #20      |      LDX #20
LOOP LDA TEXT,X   | LOOP LDA TEXT,X
     STA SCREEN,X |      STA SCREEN,X
     DEX          |      DEX
     BPL LOOP     |      BPL LOOP
     LDA #10      |      STX VAR2
     STA VAR1     |      LDA #10
     LDA #$FF     |      STA VAR1
     STA VAR2     |

     (19 bajtów)  | (17 bajtów)


ZMIANA POJEDYNCZYCH BITÓW

Chcąc zmienić tylko poszczególne bity w bajcie możecie użyć poniższego triku:

    LDA VAR1  |  LDA VAR1
    AND #$BC  |  EOR $D301
    STA VAR2  |  AND #$BC
    LDA $D301 |  EOR $D301
    AND #$43  |  STA $D301
    ORA VAR2  |
    STA $D301 |

  (16 bajtów) |  (13 bajtów)


PĘTLE

Przy prostych pętlach sterowanych rejestrami X lub Y usuwajcie rozkazy CPX i CPY:

     LDX #0    |      LDX #LEN-1
LOOP LDA SRC,X | LOOP LDA SRC,X
     STA DST,X |      STA DST,X
     INX       |      DEX
     CPX #LEN  |      BPL LOOP
     BCC LOOP  |

  (13 bajtów)  | (11 bajtów)

Powyższy przykład ma sens tylko wtedy, gdy liczba przebiegów nie przekracza 128. Aby osiągnąć to samo dla liczby przebiegów do 255 można zastosować poniższy sposób:

     LDX #0    |      LDX #LEN
LOOP LDA SRC,X | LOOP LDA SRC-1,X
     STA DST,X |      STA DST-1,X
     INX       |      DEX
     CPX #LEN  |      BNE LOOP
     BCC LOOP  |

   (13 bajtów) |  (11 bajtów)

No dobrze, ale co zrobić, kiedy licznik może być tylko zwiększany z powodu nakładania się na siebie żródłowego i docelowego obszaru pamięci? Przyjrzyjmy się temu:

     LDX #0    | OF   EQU 256-LEN
LOOP LDA SRC,X |      LDX #OF
     STA DST,X | LOOP LDA SRC-OF,X
     INX       |      STA DST-OF,X
     CPX #LEN  |      INX
     BCC LOOP  |      BNE LOOP

   (13 bajtów) |  (11 bajtów)

Jest jeszcze dużo innych konstrukcji pętli.

PARAMETRY ZA ROZKAZEM JSR

Gdy podprogram wywoływany jest wielokrotnie, wygodnie jest umieszczać parametry dla niego za jego wywołaniem. Dobrze znanym przykładem jest procedura wypisująca informacje:

PRINT PLA
      STA  VAR
      PLA
      STA  VAR+1
PRT2  INC  VAR
      BNE  PRT3
      INC  VAR+1
PRT3  LDY  #0
      LDA  (VAR),Y
      BEQ  PRT4
      JSR PUTCHAR
      JMP  PRT2
PRT4  LDA  VAR+1
      PHA
      LDA  VAR
      PHA
      RTS

VAR jest dwubajtową zmienną na stronie zerowej, a PUTCHAR - podprogramem wypisującym jeden znak. Wywołanie PRINT może wyglądać na przykład tak:

   JSR PRINT
   DTA C'To będzie wyświetlony tekst!''
   DTA 0

Nie tylko tak długie parametry jak teksty można umieszczać za JSR-em:

   LDA #155 | JSR PUT
   JSR PUT  | DFB 155

 (5 bajtów) | (4 bajty)

Ponieważ podprogram, do którego przekazuje się parametry tą metodą będzie dosyć długi, to należy użyć takiego wywołania wielokrotnie dla uzyskania efektu. Aby to jeszcze trochę skrócić, wynalazłem następujący podprogram (od tł.: w prawej kolumnie umieszczono krótszą wersję prezentowanego przez Bewesofta podprogramu):

GETSTK STX XSAVE   | GETSTK STX XSAVE
       STY YSAVE   |        STY YSAVE
       TSX         |        TSX
       INX         |        INC $103,X
       INX         |        LDA $103,X
       INX         |        TAY
       INC $100,X  |        BNE GST2
       LDA $100,X  |        INC $104,X
       INX         | GST2   LDA $104,X
       TAY         |        STA GST3+2
       BNE GST2    | GST3   LDA $FF00,Y
       INC $100,X  |        LDY YSAVE
GST2   LDA $100,X  |        LDX XSAVE
       STA GST3+2  |        RTS
GST3   LDA $FF00,Y |
       LDY YSAVE   |
       LDX XSAVE   |
       RTS         |

       (39 bajtów) | (35 bajtów)

Dzięki tej procedurze można otrzymać duży zysk pamięci, jeśli z parametrów za JSR korzysta więcej podprogramów. Zachowywanie rejestrów indeksujących można pominąć jeżeli ich zawartość nie jest potrzebna. Użycie powyższego podprogramu będzie wyglądać następująco:

    JSR PUT
    DTA 155
    ...
    ...
PUT JSR  GETSTK

* Parametr zwracany w akumulatorze...

    ...

    RTS

Dla przykładu procedura PRINT będzie wyglądać Teraz tak:

PRT2  JSR PUTCHAR
PRINT JSR GETSTK
      TAY
      BNE  PRT2
      RTS

UNIWERSALNA PROCEDURA KOPIUJĄCA

Z zastosowaniem wyżej przedstawionego GETSTK można napisać inne procedury czyniące programy krótszymi - uniwersalną procedurę kopiującą. Spójrzmy na coś takiego:

UC1   STA  VAR-252,Y
      DTA B($2C)
UCOPY LDY  #251
      JSR  GETSTK
      INY
      BNE  UC1
      TAY
UC2   DEY
      LDA  (VAR),Y
      STA  (VAR+2),Y
      TYA
      BNE  UC2
      RTS

Na stronie zerowej należy umieścić czterobajtową zmienną VAR. A oto jak stosować powyższą procedurę:

     LDX #LEN    |  JSR UCOPY
LOOP LDA SRC-1,X |  DTA A(SRC,DST)
     STA DST-1,X |  DTA B(LEN)
     DEX         |
     BNE LOOP    |

     (11 bajtów) |  (8 bajtów)

Aby osiągnąć zysk należy użyć wielokrotnie procedury UCOPY (i GETSTK). Myślę, że nie powinno być kłopotów ze znalezieniem zastosowania dla tych procedur w dużych programach. Podprogramem UCOPY można nawet zastąpić kopiowanie pojedynczego słowa:

   LDA SRC   | JSR UCOPY
   STA DST   | DTA A(SRC,DST)
   LDA SRC+1 | DTA B(2)
   STA DST+1 |

 (12 bajtów) |  (8 bajtów)

Uwaga: w przypadku, gdy ilość wywołań UCOPY jest zbyt mała, aby uzyskać jakikolwiek zysk, można nadal zaoszczędzić jeden bajt - porównajcie lewe kolumny ostatniego i przedostatniego przykładu... UCOPY może być użyta do ustawiania wartości stałych:

   LDA #1    |       JSR UCOPY
   STA DST   |       DTA A(LABEL,DST)
   LDA #15   |       DTA 2
   STA DST+1 |       ...
             | LABEL DTA 1,15

  (10 bytes) |  (10 bytes)

Cóż... Zgadza się, nie ma żadnego zysku. Lecz, gdy potrzebujesz te same zmienne wielokrotnie...

Inną zaletą UCOPY jest zerowanie akumulatora oraz rejestru Y i ustawienie znacznika Z. Dzięki temu zawartością rejestrów można od razu wyzerować jakąś komórkę pamięci, a stan znacznika Z wykorzystać do skoku warunkowego zamiast bezwzględnego. Zawartość rejestru Y można użyć jako wartości początkowej licznika pętli. Oprócz tego UCOPY nie zmienia rejestru X.

Czas na konkluzje. Sposobów na skrócenie kodu jest bardzo dużo. Można do tego użyć bardziej zwariowanych metod, np.: pobierać kod rozkazu z tablicy i umieszczać go w bardziej uniwersalnym podprogramie. Zmieniać BCS na LDA pojedynczym INCem. Użyć rozkazu BRK (tylko jeden bajt!) do częstego wywoływania procedur lub umieścić numer funkcji za rozkazem BRK (dwa bajty zamiast trzech dla JSR). I tak dalej...

Bewesoft

Od tłumacza-konsultanta Lizarda:

Korzystanie z rozkazu BRK jest możliwe lecz niesie pewne ograniczenia:

  • należy mieć pewność, że w trakcie wykonywania tego rozkazu nie wystąpi przerwanie sprzętowe,

  • z powodu złego adresu odkładanego na stos rozkaz ten traktować należy jako dwubajtowy.

  •  

    madteam.atari8.info © MadTeam, hosted: www.atari8.info