Table des matières

Retourner au sommaire

Gestion des boucles

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.

Attends, je te fais une boucle ! 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.

Boucles à compteur 8 bits

Structure classique avec DJNZ

Une chute n'a rien à voir avec une boucle ! 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

Utiliser les registres secondaires

Avec les registres secondaires, je me sens plus forte que jamais ! 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

Les registres secondaires... un vrai trésor du Z80 !

Compteur de boucle quelconque

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

Je tue les nops pour vous !

Bilan

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é.

Les boucles à compteur 16 bits

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 !

Premier jet

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.

Vroum ! Vroum ! Fais chauffer le moteur Marcel !

Premiere optimisation

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).

Creusons encore un peu

Voilà un endroit par lequel on n'a pas trop envie de faire une boucle ! À 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.

Les boucles à la pile

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

Attention, on peut vite se retrouver ensablé avec la pile !