— Basé sur l'article publié dans Quasar CPC numéro 17, Assembleur : Hardware, par OffseT.
— Il serait intéressant d'ajouter d'autres structures de boucles possibles.
Nous allons ici parler des diverses manières de gérer les boucles en assembleur Z80. Il s'agit là en effet du type de structure sur lequel de nombreuses optimisations sont possibles et cela vaut donc le coup de s'y attarder un petit peu.
Nous allons distinguer deux familles :
En ce qui concerne ces dernières, il s'agit le plus souvent d'optimiser la condition de sortie, et par là même l'algorithme plutôt que le code. Nous les laisserons donc de côté dans un premier temps pour nous intéresser exclusivement aux boucles à compteur.
Vous trouverez ci-après divers programmes d'exemples. Le corps des boucles effectue des opérations assez “stupides” mais dites vous bien qu'il est juste là pour le principe et que seule la manière dont fonctionne le bouclage est à prendre en considération.
La structure la plus classique sur Z80 est celle illustrée par le premier programme avec l'utilisation de l'instruction DJNZ
avec un compteur sur B. Elle est rapide et peu criticable sur le principe… mais ça se complique lorsque le registre B doit être utilisé dans le corps de la boucle. On se voit alors contraint d'utiliser la pile pour sauver nos registres (PUSH BC
puis POP BC
) ce qui est loin d'être performant !
; ; Structure classique ; Prog1 ld hl,&c000 ; HL=Adresse écran ld de,&4000 ; DE=Adresse données ld b,10 ; Nombre d'itérations (10) Boucle1 push bc ; Sauvegarde du compteur B ld a,(de) ; Récupération de la donnée inc de ; (DE)=Donnée suivante ld (hl),a ; Copie à l'écran call lignesuiv ; (HL)=Ligne suivante pop bc ; Récupération compteur B djnz boucle1 ; Bouclage ret ; ; Routine de calcul de la ligne suivante sur HL ; LigneSuiv ld a,8 add a,h ld h,a ret nc ld bc,&c050 add hl,bc ret
Le petit blond à lunettes nous propose une solution plus judicieuse en passant par les registres secondaires du Z80. En effet, un EXX
est bien plus rapide qu'un POP
ou un PUSH
. Ainsi, comme le montre le deuxième programme, on a alors le corps de la boucle qui travaille sur les registres secondaires sans perturber le bouclage… mais en y regardant de plus près, ça n'est pas si judicieux que ça. On utilise un plus grand nombre de registres et, en plus, on se voit dans l'obligation de couper les interruptions système pendant l'exécution du programme car celles-ci utilisent les registres secondaires !
; ; Utilisation des registres secondaires ; -> on évite des PUSH-POP dans la boucle ; MAIS ; -> on utilise beaucoup plus de registres ; -> on ralentit la partie hors boucle ; -> on doit couper les interruptions système Prog2 ld hl,&c000 ; HL'=Adresse écran ld de,&4000 ; DE'=Adresse données exx ld b,10 ; B=Compteur Boucle2 exx ld a,(de) inc de ld (hl),a call lignesuiv exx djnz boucle2 ; Bouclage ret LigneSuiv ld a,8 add a,h ld h,a ret nc ld bc,&c050 add hl,bc ret
Une solution alternative consiste à changer de compteur et à utiliser un DEC reg
suivi d'un JR NZ,adr
plutôt que le DJNZ
. Contrairement aux idées reçues, on ne perd pas en vitesse. Le troisième programme vous montre un exemple d'utilisation de A' comme compteur.
; ; Changement de compteur ; -> on évite des PUSH-POP dans la boucle ; MAIS ; -> on utilise un registre en plus (AF') ; -> on doit couper les interruptions système Prog3 ld hl,&c000 ld de,&4000 ld a,10 ; A=Compteur Boucle3 ex af,af' ld a,(de) inc de ld (hl),a call lignesuiv ex af,af' dec a jr nz,boucle3 ; Bouclage ret LigneSuiv ld a,8 add a,h ld h,a ret nc ld bc,&c050 add hl,bc ret
En conclusion, le meilleur moyen d'optimiser une boucle avec un compteur 8 bits reste celui de supprimer la boucle ! On la déroulera en répétant x fois le code. Ainsi, on gagne le temps de bouclage et éventuellement un registre (le compteur de boucle) s'il n'est pas utilisé par le corps du programme. Pour choisir le nombre d'itérations on sautera alors plus ou moins loin dans le code déroulé.
Eh oui, la situation change du tout au tout dans le cas de boucles avec un compteur 16 bits, car notre Z80 est bien plus démuni : plus d'instruction de bouclage spécifique, plus de tests rapides !
Je vous propose donc de partir d'un programme écrit rapidement sans réfléchir et de l'optimiser pas à pas.
; ; Premier programme ; Prog4 ld hl,&c000 ; HL=Adresse écran ld de,&5000 ; DE=Adresse données ld bc,&1000 ; BC=4096 itérations Boucle4 ld a,(de) ld (hl),a ; Copie de la donnée inc hl ; Octet suivant dec de ; Donnée suivante dec bc ld a,b or a ; Gestion du compteur jr nz,boucle4 ld a,c or a jr nz,boucle4 ; Bouclage ret
Dans ce quatrième programme, on utilise le registre 16 bits BC comme compteur. Comme vous le constatez, si on peut décrémenter le compteur facilement, on est obligé de tester son passage à 0 en deux temps : le passage à 0 de B, puis celui de C. C'est une structure lourde et lente.
Ah, le petit blond à lunettes est tout content car il vient de trouver une première optimisation. Il nous propose de tester le passage à zéro du registre C avant celui du registre B. En effet, rien qu'en inversant l'ordre de test on accélère le traitement global de la boucle.
; ; Optimisation de la condition de bouclage ; -> il y a moins d'intérations sur toute ; la longueur du programme ; (&10 au lieu de &100) Prog5 ld hl,&c000 ld de,&5000 ld bc,&1000 Boucle5 ld a,(de) ld (hl),a inc hl dec de dec bc ld a,c ; On teste l'octet de or a ; poids faible en jr nz,boucle5 ; premier... ld a,b or a jr nz,boucle5 ret
Si on décortique le fonctionnment de la boucle, on remarque que lorsque B sera arrivé à 0, il gardera cette valeur pendant 256 itérations, le temps que C passe à ton tour à zéro. Dans le cas du programme 4, le bouclage s'effectuera donc 256 fois sur le test de C en testant malgré tout B à chaque fois ! C'est une énorme perte de temps qu'on minimise si on utilise plutôt ce que nous propose le petit blond à lunettes avec le programme 5. Il est tout fier de lui car dans le cas de son programme, pour BC partant de 4096, le test de bouclage ne se fera que 16 fois sur B (le test externe).
À mon tour de dire que ça reste 16 itérations de trop. En réfléchissant un peu plus, on peut se réduire à une condition de bouclage sur un registre 8 bits. Je vous propose le programme 6 dans lequel on n'a plus besoin d'un double test car le test de B suffit. Il s'agit tout simplement de revoir notre façon de compter et de tourner le désavantage d'un compteur sur 16 bits en notre faveur.
; ; Meilleure optimisation de la condition ; de bouclage ; -> la condition de bouclage est simplifi{e ; et on peut accélérer la boucle Prog6 ld hl,&c000 ld de,&5000 ld bc,-&1000 ; On part d'un compteur Boucle6 ld a,(de) ; négatif que l'on ld (hl),a ; incrémente jusqu'à 0 inc hl dec de inc bc ld a,b or c jr nz,boucle6 ret
Au lieu de partir de 4096 pour descendre jusqu'à 0, on part de -4096 pour monter jusqu'à 0. Dans ce cas les registres B et C s'annuleront en même temps à la dernière itération. De plus, B ne s'annulera que lors de cette dernière itération, il est donc le seul registre à tester. Voici comme on parvient à effectuer un test 8 bits pour gérer un bouclage 16 bits. Ce programme est nettement plus avantageux que les deux précédents et ne souffre d'aucun contrainte.
Enfin, si… il y a une légère contrainte. La valeur d'initialisation du compteur BC ne doit pas annuler B ! C'est-à-dire qu'elle doit être supérieure à &100. Si vous ne suivez pas tout, c'est que vous avez besoin de réviser vos nombres signés en binaire. Passons maintenant à la dernière optimisation envisagée ici et qui constitue un nouveau type de boucle.
Je vois que le petit blond à lunettes est sceptique. “Quel rapport entre la pile et les boucles ?” Eh bien, il est tout à faire possible d'utiliser une pile d'adresses de bouclage à la place d'un compteur ! Cette méthode a de nombreux avantages.
Tout d'abord, on évite ainsi toute forme de test de bouclage ce qui fait gagner du temps et des registres. En revanche, on doit utiliser plus de mémoire (pour la pile) et on doit faire attention aux interruptions. Le programme 7 illustre l'utilisation d'une pile ; on voit bien que si une interruption devait avoir lieu pendant la boucle, celle-ci utiliserait notre pile de bouclage pour sauver PC et celle-ci serait donc inutilisable une deuxième fois.
; ; Utilisation d'une pile ; -> on supprime la condition de bouclage ; MAIS ; -> on utilise plus de mémoire ; -> on ralentit la partie hors boucle ; -> attention aux interruptions ! Prog7 call prepile ; Préparation de la pile ld hl,&c000 ; de bouclage ld de,&5000 ld (pilesys),sp ; Sauvegarde de la pile ld sp,pile ; système et installation Boucle7 ld a,(de) ; de la pile de bouclage ld (hl),a inc hl dec de ret ; Bouclage Fin ld sp,(pilesys) ; Récupération de la pile ret ; système Pile dw boucle7 ; Pile de bouclage ds 4094*2 ; 4095 adresses de bouclage dw fin ; 1 adresse de sortie PileSys dw 0 ; Pointeur pile système ; ; Routine de préparation de la pile de bouclage ; PrePile ld hl,pile ; Copie de l'adresse ld de,pile+2 ; de bouclage dans toute ld bc,4094*2 ; la pile sauf l'adresse ldir ; de fin de bouclage ret