PROGRAMOWANIE PROCESORA 6502 W KOMPUTERACH ATARI XL/XE
Optymalizacja programów
Optymalizacją nazywamy takie zmiany w gotowym, działającym programie, by pewne (założone) parametry tego programu zmieniły się na lepsze. Warunek poprawnego działania jest bardzo ważny, ponieważ optymalizowanie programu na ogół zmniejsza jego czytelność, utrudnia przeto późniejsze poprawki i uzupełnienia; zwiększa się podatność programu na wprowadzenie trudnych do zlokalizowania błędów. Optymalizowanie programu działającego źle (lub wcale) mija się w ogóle z celem.
Możemy tylko zazdrościć "dużym" komputerom, dla których napisano wiele systemów automatycznej optymalizacji. Niestety, my musimy to robić ręcznie (tak się mówi, choć w pracy tej największy udział ma głowa). Optymalizowanie jest czynnością trudną i niebezpieczną. Przed rozpoczęciem tęgo procesu dobrze jest wykonać kopię tekstu programu, bo być może trzeba będzie do mej wrócić, gdy "przeoptymallzowany" program w ogóle przestanie działać.
Z wielu możliwych celów optymalizacji najczęściej stawiamy sobie dwa:
zwiększenie szybkości lub zmniejszenie rozmiarów programu. Niestety, rzadko udaje się pogodzić te postulaty.
Aby złagodzić zjawisko pogarszania się przejrzystości programu stosujemy komentarze opisujące istotę dokonanej modyfikacji, np.:
LDA #0
JMP DALEJ
zamieniamy na:
LDA #0
BEQ DALEJ (zawsze, bo Z=l)
Tym sposobem zyskujemy jeden bajt, ale uzależniamy rozkaz BEQ od poprzedniego. Jeżeli w przyszłości zajdzie potrzeba zmiany pierwszego rozkazu na:
LDA #1
mamy szansę, dzięki komentarzowi, pamiętać o konieczności zmiany warunku skoku.
Zwiększenie szybkości przy niezmienionych rozmiarach
Ten typ optymalizacji jest pierwszym, najłagodniejszym sposobem przyspieszania bez zbytniego zaburzania struktury programu. Polega on na takim przegrupowaniu rozkazów, aby wyrzucić z wnętrza pętli te rozkazy, które nie muszą być wykonywane za każdym razem. Napisałem tak:
LDY #39
ZERUJ LDA #0
STA (EKR),Y
DEY
BPL ZERUJ
dla wyzerowania na ekranie pewnego wiersza, którego adres znajduje się w słowie EKR. Odzwierciedla to mój sposób myślenia w chwili tworzenia algorytmu: umieszczam w akumulatorze 0, przesyłam je do pamięci, to samo dla sąsiedniej komórki, i tak dalej... Ale uważny rzut oka upewnia nas, że zawartość akumulatora nie jest nigdzie wewnątrz pętli zmieniana. Nie ma więc powodu, by odnawiać ją tyle razy. W zupełności wystarczy:
LDY #39
LDA #0
ZERUJ STA (EKR),Y
DEY
BPL ZERUJ
Teraz pętla ZERUJ wykonuje się znacznie szybciej. Inny przykład: zerujemy 10 stron pamięci:
LDX #10
LDA #0
Z LDY #0
ZERUJ STA (ADDR),Y
INY
BNE ZERUJ
INC ZERUJ ADDR+1
DEX
BNE Z
Widać jednak, że zewnętrzna pętla zaczyna się od nadania rejestrowi Y wartości 0 i taka też wartość pozostaje w nim w chwili opuszczenia pętli. Można więc z powodzeniem usunąć etykietę Z, a ostami wiersz zamienić na:
BNE ZERUJ
Oczywiście dziewięciokrotne niewykonanie rozkazu LDY daje niewielki zysk, ale może się on kumulować, jeżeli taki fragment jest w programie wywoływany wielokrotnie.
Jednoczesne zwiększenie szybkości i zmniejszenie rozmiarów
Najbardziej pożądany wariant. Polega na usuwaniu zbędnych rozkazów lub zamianie dłuższych sekwencji na krótsze o takim samym działaniu. Przeciętny, dobrze napisany program zawiera całe mnóstwo zbędnych rozkazów. Na etapie konstruowania i testowania programu nie troszczymy się o to, ponieważ nadrzędnym celem jest przejrzystość. Potem zaczyna się zabawa w "wyciskanie wody". Nieraz zakładam się z kolegami, że KAŻDY program można skrócić o jeden bajt. Oczywiście, nie jest to prawda absolutna, lecz w praktyce sprawdza się na tyle często, że wygrywam prawie każdy taki zakład. Nie sposób opisać wszystkich możliwości, pokażę więc kilka typowych przykładów.
Kopiowanie zawartości rejestru
polega na "pożyczeniu" potrzebnej wartości z innego rejestru zamiast pobierania jej z pamięci. W poprzednim przykładzie
LDA #0
LDY #0
można zastąpić przez:
LDA #0
TAY
z zyskiem jednego bajtu.
Wykorzystanie zawartości rejestru polega na zaniechaniu umieszczania w rejestrze nowej wartości, gdy wiemy, że poprzednia była taka sama. Przykład: umieszczenie w pamięci adresów dwóch kolejnych wierszy ekranu.
LDA EKRAN
STA ADR1
STX ADR1+1
LDA EKRAN+40 (l)
STA ADR2
STX ADR2+1
W przeważającej większości przypadków oba adresy dotyczą tej samej strony pamięci i rozkaz oznaczony (!) można usunąć z programu.
Bardzo często pętle kończą się z wartością 255 (czyli -1) w rejestrze indeksowym, jak to ma miejsce w pierwszym z przykładów zerowania. Jeżeli po takiej pętli następuje rozkaz
LDY #0
to można go z powodzeniem zastąpić przez
INY
Wykorzystanie stanu znacznika polega na zaniechaniu ustawiania znacznika, którego stan jest znany. Przytaczana już na tych stronach uproszczona konwersja ATASCII na kod ANTIC-a może zawierać sprawdzenie, czy liczba jest większa od 96, bo wtedy pozostawiamy ją bez zmian:
CMP #96
BCS OK
W przeciwnym razie trzeba od niej odjąć 32:
SEC
SBC #32
Ponieważ jednak po nieskutecznym skoku BCS znacznik C ma zawsze wartość 0, możemy powyższą parę rozkazów zastąpić jednym, z takim samym skutkiem:
SBC #31
Zawsze skasowany znacznik C spowoduje odjęcie dodatkowej jedynki, dlatego niezbędne jest skorygowanie argumentu.
Użycie właściwego trybu indeksowego dla strony zerowej. Jak
wiemy, w repertuarze rozkazów naszego procesora brak jest krótkiej formy rozkazu
LDA Z0, Y
dla Z0 zadeklarowanego jako wartość z zakresu 0..255 i dlatego asembler tłumaczy to na trzybajtowy rozkaz maszynowy (dwubajtowy argument). Zamiana rejestru
LDA Z0,X
sprawi, że rozkaz ten będzie miał 2 bajty długości (argument jednobajtowy). Ten zabieg może niestety wymagać zmian w koncepcji wykorzystania rejestrów w okolicznych częściach programu, dlatego lepiej mieć to na uwadze już podczas wstępnego komponowania algorytmu.
Wiązanie procedur polega na zmianie sposobu wywoływania procedur z wnętrza innych procedur. Jeżeli procedura X wywołuje procedurę Y tak:
X LDY #0
RTS
Y LDX #0
JSR Y (!)
RTS (!!)
to można z powodzeniem pominąć rozkaz oznaczony (!!), a poprzedni zamienić na
JMP Y (!)
Teraz jednak wystarczy zamienić kolejność tych dwóch procedur, aby rozkaz oznaczony (!) można było całkowicie pominąć.
Zwiększenie szybkości kosztem rozmiarów
Rozwijanie pętli polega na zamianie pętli na niecykliczną sekwencję rozkazów. Przykładowo, zerowanie pięciu kolejnych bajtów od adresu TAB:
LDY #4
LDA #0
ZERUJ STA TAB,Y
DEY
BPL ZERUJ
zajmuje 10 bajtów i wymaga od procesora wykonania 17 rozkazów. Równoważny temu fragment:
LDA #0
STA TAB
STA TAB+1
STA TAB+2
STA TAB+3
STA TAB+4
zajmuje 17 bajtów, lecz wymaga wykonania tylko 6 rozkazów.
Stosowanie tablic zamiast obliczeń polega na wstępnym przygotowaniu tablic wartości, z których trzeba często korzystać. Wykorzystanie tablicy do konwersji kodów ATASCII na kody ANTIC-a pokazałem w TA 9/92. Innym popularnym zastosowaniem tej metody jest tablicowanie wartości funkcji trygonometrycznych. Na początku programu "na sucho" oblicza się WSZYSTKIE wartości, z których będziemy później korzystać. Później, podczas normalnej pracy program nie odwołuje się już do znanych ze swej chyżości funkcji w ROM-ie ATARI, lecz czerpie gotowe wartości z tablicy.
Zmniejszanie rozmiarów kosztem szybkości
Zwijanie sekwencji w pętlę polega na zastępowaniu serii rozkazów pętlą. Typowym przykładem jest przechowywanie wektorów dwóch przerwań:
LDA VVBLKI
STA OLDVBI
LDA VVBLKI+1
STA OLDVBI+1
LDA VVBLKD
STA OLDVBD
LDA VVBLKD+1
STA OLDVBD+1
Zajmuje to 24 bajty, gdy słowa OLDVBI i OLDVBD znajdują się poza stroną zerową. Po zwinięciu w pętlę:
LDX #1
CHOWA LDA VVBLKI,X
STA OLDVBI,X
LDA VVBLKD,X
STA OLDVBD,X
DEX
BPL CHOWA
fragment równoważny pod względem efektu zajmuje już tylko 17 bajtów.
Zastępowanie podobnych sekwencji procedurą polega na odszukiwaniu fragmentów programu podobnych do siebie i zastępowaniu ich wywołaniem osobno stojącej procedury. Typowym przykładem jest dodawanie do pewnego słowa w pamięci różnych liczb jednobajtowych. Taka potrzeba występuje nieraz podczas bezpośredniego wyświetlania danych na ekranie:
CLC
LDA ADDR
ADC #n
STA ADDR
BCC OK
INC ADDR+1
OK EQU *
Jeżeli takich fragmentów jest kilka (wiele?), a zmienia się tylko n, to można je zastąpić przez:
LDA #n
JSR DODAJ
i stworzyć osobną procedurę:
DODAJ CLC
ADC ADDR
STA ADDR
BCC OK
INC ADDR+1
OK RTS
Podane przykłady nie wyczerpują wszystkich sposobów optymalizacji - jest ich bez liku. Tym większe pole do popisu dla miłych Czytelników. Pamiętajcie: każdy program można udoskonalać bez końca. Ale po co?...
Janusz B. Wiśniewski
|