Zgodnie z obietnicą wziąłem się za napisanie kolejnej części artykułu o programowaniu małych, rozmiarowo, programów. Tym razem, zgodnie z sugestiami czytelników, postanowiłem oprzeć moje wywody na przykładzie prostego programu. Zachęcam tym samym czytelników do śledzenia ze mną procesu optymalizacji kodu.
Program, który postanowiłem napisać to prosta procedura, prezentująca owalny kształt złożony z linii, który ostatecznie tworzy coś na kształt wycinanki kurpiowskiej albo Gwiazdy Śmierci z uniwersum Star Wars. Nie jest to nic szczególnego, ale w tym przypadku nie jest tak ważne, co program prezentuje, gdyż jest to tylko tło do rozważań na temat zmniejszenia rozmiaru pliku. Oto wstępny program wraz ze zdjęciem, pokazującym jego działanie. os_drawto equ $f9c2 ;drawto system procedure starty equ $5A ;start of Y startx equ $5B ;start of X atachr equ $2fb ;color endy equ $54 ;end of Y endx equ $55 ;end of X
loop_draw lda start_ asl asl tay lda sinus,y sta startx lda sinus+$40,y sta starty lda end_ asl asl tay lda sinus,y sta endx lda sinus+$40,y sta endy
jsr os_drawto
lda end_ clc adc #4 sta end_ dec counter+1 counter lda #$0d bne loop_draw lda start_ clc adc #5 sta start_ sta end_ for_counter lda #$0c sta counter+1 dec for_counter+1 bne loop_draw
infinite beq infinite
run $80
Program w obecnej formie zajmuje 126 bajtów. Zobaczymy, o ile uda się go zmniejszyć. Jak widać procedura nie jest zbyt skomplikowana, jednak postaram się omówić skrótowo jej działanie. Na początku deklaruję adresy zmiennych systemowych, adres tablicy sinusa oraz adres procedury drawto, która odpowiada za stawianie linii. startx, starty, endx i endy to zmienne określające współrzędne linii, która ma być narysowana. Zmienne start_ oraz end_ będą zawierały w sobie wartości kolejnych linii.
W dalszej części zawarty został generator sinusa, autorstwa Koali z grupy Agenda, który generuje przebieg tej fukcji dla wartości od 0 do 64 z dodanym offsetem #$40 (64 dziesiętnie), aby linie rysowały się bliżej centrum ekranu. Wartości sinusa są bezpośrednio wartościami bowiem współrzędnych linii, które są rysowane w programie.
W dalszej części kodu deklarowane są wartości zmiennych start_ i end_ oraz ustalany jest kolor pixela na zapalony (sty atachr). Następnie program przechodzi do głównej pętli. Wartości start_ i end_ stanowią podstawę do iteracji po tablicy sinusów i stamtąd są wpisywane wartości dla kolejnych linii. Jak czytelnik może zauważyć, wartość dla zmiennych Y jest brana z tablicy sinusa z przesunięciem o #$40 (64 dziesiętnie). Dzieje się tak dlatego, że aby narysować okrąg należy w rejestrach X i Y wpisać wartości sinusa i cosinusa danego kąta, a wartość cosinusa to po prostu wartość sinusa z przesunięciem 90 stopni, co w naszym przypadku równa się #$40, czyli 1/4 tablicy.
Wartości end_ i start_ są mnożone przez 4 (asl/asl), aby rysowane linie nie były tak gęsto umiejscowione. Po każdej narysowanej linii wartość end_ jest zwiększana o 4, tak, że każda linia rysuje się ze stałym przyrostem na obrzeżach okręgu. Następnie zmnejszana jest wartość zmiennej counter i jeśli osiągnie ona zero to rysowanie linii jest przerywane i program przygotowuje się do rysowania kolejnej partii linii, tym razem z inną wartością startową. Wartość zmiennej start_ jest zwiększana o 5, a wartość zmiennej counter, która właśnie osiągnęła zero, jest przywracana do nowej wartości, tym razem jednak o jeden mniejszej, w ten sposób każda partia linii rysuje się w mniejszej liczbie.
Kiedy zmienna for_conter osiągnie zero, program przechodzi do nieskończonej pętli infinite. Czas na optymalizację! 126 bajtów to niezły wynik, jednak kod posiada szereg rzeczy, które można skrócić. Na początek pozbywamy się pseudorozkazu run $80. W programach, gdzie jest do pamięci ładowany tylko jeden blok (czyli występuje w nim tylko jedno polecenie org), nie ma potrzeby deklaracji, od którego miejsca program na startować. Większość dosów i loaderów bowiem tego nie potrzebuje, a oszczędza to 6 bajtów pliku.
Kolejną rzeczą jest pozbycie się deklaracji zmiennych start_ oraz end_: ldy #0 sty start_ sty end_
Jak widać potrzebujemy wartości 0 w obu komórkach. Szybki rzut oka na debugger pozwala stwierdzić, że po początkowym działaniu programu komórki $88 oraz $99 zawierają zera, więc po prostu pozbywamy się powyższej deklaracji, a zamiast tego zmieniamy umiejscowienie naszych zmiennych na początku programu:
start_ equ $88 end_ equ $99
Dobrze by było również uniknąć deklaracji zmiennej atachr, która odpowiada za kolor stawianego piksela. Domyślnie jest to kolor tła, więc musimy to zmienić, ale szybka próba usunięcia rozkazów: ldy #0 iny sty atachr
i zastąpienie ich po prostu: sty atachr
wydaje się działać, dlatego, że poprzednia procedura ustawia rejestr Y na odpowiednią wartość pod koniec działania. W ten sposób program zmniejszył się do 113 bajtów.
Następną rzeczą, której możemy się pozbyć, jest mnożenie wartości start_ oraz end_ za każdym razem przez 4, poprzez podwójny rozkaz asl, który przesuwa bity w lewo, co równa się mnożeniu przez dwa. Jeśli i tak za każdym razem przy rysowaniu nowej linii zwiększamy wartość end_ o 4, to równie dobrze możemy to robić o 16, redukując kod z takiego: lda end_ asl asl lda sinus,y sta endx lda sinus+$40,y sta endy
jsr os_drawto
lda end_ clc adc #4 sta end_
do takiego: ldy end_ lda sinus,y sta endx lda sinus+$40,y sta endy
jsr os_drawto
lda end_ clc adc #16 sta end_
Również wartość start_, która jest zmieniana w następnej kolejności o 5, może zmieniać zawartość równie dobrze o 20: lda start_ clc adc #20 sta start_
Dzięki temu zabiegowi program ma już tylko 107 bajtów. Teraz przyszedł czas na próbę usunięcia rozkazów clc, które zerują znacznik C, dzięki temu wynik dodawania jest większy o jeden. Po usunięciu tych rozkazów nie widać większego wpływu na kod, więc spokojnie można zastosować ten skrót: lda end_ adc #16 sta end_
[...]
lda start_ adc #20 sta start_
Program dzięki temu nasz program jest teraz o kolejne dwa bajty krótszy i ma 105 bajtów, czyli zmniejszyliśmy go o łącznie 22 bajty. Na tym kończą się moje pomysły na optymalizację tak na szybko, jednak znając życie czytelnicy w komentarzach zasugerują kolejne zmiany. Mam nadzieję, że ten krótki cykl artykułów znalazł uznanie czytelników AtariOnline.pl. Na koniec czas na kod już po optymalizacji: os_drawto equ $f9c2 ;drawto system procedure starty equ $5A ;start of Y startx equ $5B ;start of X atachr equ $2fb ;color endy equ $54 ;end of Y endx equ $55 ;end of X
start_ equ $88 end_ equ $99
sinus equ $2000 org $80
lda #8 jsr $ef9c ;setting system graphics mode
ldy #$7f ; txa ;nie ma przymusu zerowanie akumulatora (nie zauważyłem jakiegoś większego wpływu na tworzoną tablicę) loop __1 adc #$00 bvc __2 inc __3+1 __2 inc __1+1 pha __4 lda #0 lsr bcc skp tax __3 lda #$40 ;<- za pomocą tego można przesówać na inne świartki sinusa sta sinus+$00,x sta sinus+$80,y sta sinus+$100,x dey skp pla inc __4+1 bne loop
sty atachr
loop_draw ldy start_ lda sinus,y sta startx lda sinus+$40,y sta starty ldy end_ lda sinus,y sta endx lda sinus+$40,y sta endy
jsr os_drawto
lda end_ adc #16 sta end_ dec counter+1 counter lda #$0d bne loop_draw lda start_ adc #20 sta start_ sta end_ for_counter lda #$0c sta counter+1 dec for_counter+1 bne loop_draw
Faktycznie, można na końcu programu zrobić ".byte $0c" i odwoływać się do tej komórki przy kolejnych przebiegach pętli, czyli zamiast Dec counter+1 Counter Lda #$0c Bne loop_draw Zrobić: Dec counter Bne loop_draw (...) Counter .byte $0c , Dzięki ,a uwagę Ery, w ten sposób oszczędziliśmy jeden bajt. --- Jeszcze jedna optymalizacja wpadła mi do głowy Procedura os_drawto zaczyna się od wczytania do akumulatora którejś z wartości x lub y(teraz jestem poza komputerem i nie sprawdzę), więc zamiast robić dajmy na to Lda sinus+$40,y Sta endxy Jsr os_drawto
Można zrobić Lda sinus+$40,y Jsr os_drawto+2
jhusak @2023-07-03 14:36:09
Fajnie widać te przybliżone parabolami sinusy :) Koło takie trochę kwadratowe :)
BartGo @2023-07-03 14:42:03
a czy można prosić o jakiś pseudokod opisujący jakie wzory stoją za powyższym kodem w asemblerze? może ciekawe mogłoby być podejście do tego samego zagadnienia w innym języku i ocena wielkości kodu...
gorgh @2023-07-03 16:14:59
Ja się nie podejmę opisania tego w pseudokodzie, cały fun w sizecodingu to praca w assemblerze
A co powiedzielibyście na taką wersję (ok, sorki za przepisanie na MADS-a, ale tak mi się lepiej myśli...)
OS_DRAWTO = $f9c2 ;drawto system procedure STARTY = $5A ;start of Y STARTX = $5B ;start of X ATACHR = $2fb ;color ENDY = $54 ;end of Y ENDX = $55 ;end of X START_ = $88 END_ = $99 SINUS = $2000 org $80
lda #8 jsr $ef9c ;setting system graphics mode
ldy #$7f LOOP ADC1 adc #$00 bvc ADC2 inc ADC3+1 ADC2 inc ADC1+1 pha LDA4 lda #0 lsr bcc SKP tax ADC3 lda #$40 ;<- za pomocą tego można przesuwać na inne ćwiartki sinusa sta SINUS+$00,x sta SINUS+$80,y sta SINUS+$100,x dey SKP pla inc LDA4+1 bne LOOP
sty ATACHR
LOOP_DRAW ldy START_ lda SINUS,y sta STARTX lda SINUS+$40,y sta STARTY ldy END_ lda SINUS,y sta ENDX lda SINUS+$40,y sta ENDY
jsr OS_DRAWTO
lda END_ adc #16 sta END_ dec COUNTER+1 COUNTER lda #$0d bne LOOP_DRAW lda START_ adc #20 sta START_ FOR_COUNTER lda #$0c sta COUNTER+1 dec FOR_COUNTER+1 bne LOOP_DRAW
INFINITE beq INFINITE
JacekPiast @2023-07-05 08:25:50
Super, dzięki za kolejną fajną lekcję! :)
gorgh @2023-07-05 10:37:52
UnDead: na pierwszy rzut oka nie widzę różnicy a nie mam jak sprawdzić kodu bo do wtorku jestem poza komputerem
Adam @2023-07-05 12:44:31
Wersja UnDeada OK, ale średnio mi wizualnie przypomina Gwiazdę Śmierci :)
A ponadto proponuję ewentualne listingi wrzucać na forum, ze znacznikami [ code ] [/code] to będzie znacznie czytelniejsze niż tu w komentarzach, bo ten system sobie z tym, jak widać, nie radzi.
@gorgh w zasadzie ma mniej o jedno sta ;-) Ale efekt spodobal mi się na tyle, że dalej mamy efekt... kulki, a udało się jeszcze trochę urwać. Eksperymentowałem jeszcze ze zredukowaniem pętli z dwóch do jednej i otrzymałem coś, co wygląda jak logo śp. Daewoo, ale położone bokiem, więc już za daleko odbiegające od oryginału ;-)
OK, jeszcze jeden z moich eksperymentów dał ładny efekt, ale ani to kula, ani Gwiazda Śmierci:
[code]OS_DRAWTO = $f9c2 ;drawto system procedure STARTY = $5A ;start of Y STARTX = $5B ;start of X ATACHR = $2fb ;color ENDY = $54 ;end of Y ENDX = $55 ;end of X START_ = $A8 END_ = $B9 SINUS = $2000 org $80
lda #8 jsr $ef9c ;setting system graphics mode
ldy #$7f LOOP ADC1 adc #$00 bvc ADC2 inc ADC3+1 ADC2 inc ADC1+1 pha LDA4 lda #0 lsr bcc SKP tax ADC3 lda #$40 ;<- za pomocą tego można przesuwać na inne ćwiartki sinusa sta SINUS+$00,x sta SINUS+$80,y sta SINUS+$100,x dey SKP pla inc LDA4+1 bne LOOP
sty ATACHR
LOOP_DRAW ldy START_ lda SINUS,y sta STARTX lda SINUS+$40,y sta STARTY ldy END_ lda SINUS,y sta ENDX lda SINUS+$40,y sta ENDY
jsr OS_DRAWTO
dec COUNTER+1 COUNTER lda #$0d bne LOOP_DRAW lda START_ adc #20 sta START_ FOR_COUNTER lda #$0c sta COUNTER+1 dec FOR_COUNTER+1 bne LOOP_DRAW
INFINITE beq INFINITE[/code]
UnDead @2023-07-05 19:49:22
@Kaz coś taki [code][/code] mi tu nie zadziałały... :(
Personally I am not a fan of these 256, 128, 64 or 32 Byte demos. I do prefer a) the 16K intros with various effects and music and b) the mega-demos for 64k-320k machines (not much love for the 576k and 1088k demos, since loading time is way too long).
I rarely watch these small size coded byte demos, most of the time I watch them once and then never again (also do not collect them). But maybe someone could create a 16K or 64K demo with lots of these small size demos as one big(ger) fx demo ?
Of course this is just my personal (and fully subjective) opinion. For me it is sad, that big(ger) demos and intros are getting less and less on the A8.
gorgh @2023-07-10 09:02:04
Charlie Chaplin: I usually also don't run tiny intros on my Atari, but I admire them- it's always some sort of craftmenship and technical achievement and it's a pleasure to know someone was ale to do such effects in such tiny space e.g. latest intro by Superogue released on Lost Party