Exemple d'optimisation assembleur sur Commodore 64
En cherchant des bouts de code à propos de tout et de rien pour Commodore 64, je suis tombé sur un tutoriel montrant l’effet color wash. Je ne sais pas si c’est le nom usuel, ni même si cet effet à un nom spécial, mais c’est comme ça que l’auteur du tutoriel l’appelle. C’est donc comme ça que je vais l’appeler.
Vous pouvez voir l’effet en question sur youtube : https://www.youtube.com/watch?v=iUVFHxnvabM Et vous trouverez le code original sur github : https://github.com/actraiser/dust-tutorial-c64-first-intro/blob/master/code/sub_colorwash.asm
Je voulais étudier cet effet en détail, mais la lecture du code ne m’a pas été très utile pour comprendre du mieux possible comment ça fonctionnait. J’ai donc chercher à le refaire par moi-même et ensuite à l’optimiser si possible. Et ça a été possible ;)
Mon code sur github (seulement l’effet visuel, pas de musique) : colorwash
Dans un sens…
Dans un premier temps je me concentre sur une seule ligne de texte, l’effet se déplaçant de droite à gauche. Pour comprendre ce qu’il faut faire, je trouve souvent qu’un peu de pseudo-code m’est d’une grande aide. On a donc un tableau rempli de N couleurs. On a aussi LIGNE, qui est l’adresse du texte dans la color RAM du C64 :
couleur = [1,1,2,9,7,2,...,N]
Le but est de faire une rotation de chaque case du tableau couleur, et une rotation identique en color RAM :
temp = couleur[0]
pour i = 1 à N-1
c = lire couleur[i]
couleur[i-1] = LIGNE[i-1] = c
couleur[N-1] = temp
Après m’être convaincu que ça marche en faisant tourner ce pseudo-code dans ma tête, je le transforme en assembleur 6502 :
lda color
sta temp
ldx #0
loop:
lda color+1,x
sta color,x
sta COLOR_LINE_1,x
inx
cpx #39
bne loop
lda temp
sta color+39
Pour bien comprendre, sachez que COLOR_LINE_1 est l’adresse de la première
ligne de texte en color RAM, et que color est le tableau de 40 couleurs :
.const COLOR_LINE_1 = $d990
color:
.byte 9,9,2,2,8,8,10,10,15,15,7,7,1,1,1,1,1,1,1,1
.byte 1,1,1,1,1,1,1,1,7,7,15,15,10,10,8,8,2,2,9,9
…puis dans l’autre
Maintenant je travaille sur l’effet de gauche à droite. L’idée est de faire l’inverse du premier algo. Voici le pseudo-code :
longueur = longueur du tableau de couleur
temp = couleur[39]
pour i = longueur-1 à 1
c = lire couleur[i-1]
couleur[i] = LIGNE[i] = c
couleur[0] = temp
Et le voici en assembleur :
lda color2+39
sta temp2
ldx #39
first_line:
lda color2-1,x
sta color2,x
sta COLOR_LINE_2,x
dex
cpx #0 // inutile, mais fait le parallèle avec l'autre
bne first_line
lda temp2
sta color2
Techniquement l’instruction cpx #0 est inutile dans ce sens de rotation. Mais
je la fait apparaître pour accentuer la similarité des deux algorithmes.
J’ai donc fini par obtenir plus ou moins la même chose que le code original. J’ai le même nombre d’instructions, même si j’utilise deux variables temporaires qui ne sont pas nécessaire dans la version d’origine de l’auteur. Mais j’avais ma petite idée derrière la tête pour faire cela et on va le voir tout de suite.
Maintenant je vais tenter de combiner les deux boucles
Les deux algos n’utilisent que deux registres du 6502. On pourrait donc remplacer le registre X par le registre Y dans l’un ou l’autre des algos, ce qui va grandement faciliter la combinaison.
lda color+39
sta temp
lda color2
sta temp2
ldy #39
ldx #0
lines:
lda color-1,y
sta color,y
sta COLOR_LINE_1,y
lda color2+1,x
sta color2,x
sta COLOR_LINE_2,x
inx
dey
bne lines
lda temp
sta color
lda temp2
sta color2+39
On a gagné 2 ou 3 instructions, mais surtout, on a économisé pas mal de temps de boucle.
Finalement on utilise directement la COLOR RAM
Plutôt que d’utiliser des tables pour faire la rotation des couleurs, ne pourrait-on pas faire tourner ces couleurs directement dans la color ram ? Histoire de gratter encore quelques instructions.
D’abord on initialise la color ram. On a toujours besoin d’une table pour cette initialisation (mais une seule, non plus deux) :
ldx #0
lines:
lda color,x
sta COLOR_LINE_1,x
sta COLOR_LINE_2,x
inx
cpx #40
bne lines
Une fois cette mise en couleur initiale du texte, on peut alors intervenir directement dans la mémoire couleur en faisant l’économie de deux instructions au passage :
lda COLOR_LINE_1+39
sta temp
lda COLOR_LINE_2
sta temp2
ldy #39
ldx #0
lines:
lda COLOR_LINE_1-1,y
sta COLOR_LINE_1,y
lda COLOR_LINE_2+1,x
sta COLOR_LINE_2,x
inx
dey
bne lines
lda temp
sta COLOR_LINE_1
lda temp2
sta COLOR_LINE_2+39
Conclusion
Comme je ne me suis pas tapé tout ça juste pour le “ressenti” que c’est effectivement optimisé, je compte les cycles utilisés dans chaque version pour avoir une mesure objective.
Dans la version initiale on compte 836 cycles dans un sens et 758 dans l’autre
sens (car il y a un cpx en moins), pour un total de 1594 cycles.
Dans ma version finale je compte seulement 1012 cycles. Pas mal ;)
Si vous trouvez comment faire encore plus rapide, surtout n’hésitez pas à me contacter.
Commentaires
Pas encore trouvé de solution simple et non-invasive pour avoir des commentaires sur le blog. En attendant vous pouvez laisser votre Commentaire sur mastodon@lkdjiin