Optimisations  z80
Écrit par Chris?
 

Techniques d'optimisation pour le z80 sur les TI

L'optimisation du z80 me rappelle les vieux jours des processeurs de la famille 8088. C'était cruel, mais un bon programmeur pouvait en extraire le maximum avec quelques techniques d'optimisation. Ce texte a pour but d'améliorer vos capacités d'optimisation du Z80 pour que vous puissiez mettre tous les effets géniaux que vous souhaitiez ajouter dans vos jeux, applications, etc...

 

Le BUS, probablement le pire moyen de transport...

Le BUS dans les puces électroniques peut être considéré comme une façon paresseuse de faire les circuits d'une puce! Il s'agit d'utiliser le BUS comme un intermédiaire de la mémoire vers les registres et de charger les operands/opcode. C'est une sorte d'autoroute =). Donc si vous voulez charger quelque chose à partir de la mémoire, vous le chargez d'abord dans le bus et la seconde étape est de transférer l'information du bus vers le registre. Honnêtement, son but est de réduire le nombre de fils et de portes logiques pour réduire les coûts, car il serait assez dur d'avoir 4Meg sans BUS <lol>. De toute façon vous le constaterez par vous-même si vous ouvrez un livre à propos des circuits internes des ordinateurs ;).

Pour des raisons d'optimisation, vous pouvez considérer le BUS comme un registre intermédiaire (amener l'information d'un endroit à un autre). Le Z80 doit aller chercher chaque instruction qu'il exécute. Avec son bus de 8 bits, cela signifie qu'il lui faut un total de 4 cycles pour amener un seul octet dans une file pour qu'il soit traité. Heureusement, le processus de recherche peut être fait en même temps qu'une instruction est exécutée. Mais un problème apparaît lorsque vous avez une instruction de 4 cycles qui fait plus de 1 octet par exemple dans une file vide.

Supposons l'instruction ld a, 1 Cette instruction fait 2 octets de long et prends cycles selon Zilog. 7 cycles ? Pas vraiment, comme cette instruction fait 2 octets, il faut 2x4 cycles pour amener l'instruction ce qui signifie que vous arrivez en fait à 8 cycles.


Figure 1.1

Maintenant supposons la suite d'instruction suivante

ld a, 4
push af


Il faut 8 cycles pour charger la première commande comme nous avons vu  et il faut ensuite un total de 11 cycles pour exécuter le push qui ne fait que 4 octets de long. Il y a largement assez de temps pour amener l'instruction. Comme ça prend plus de temps d'exécuter l'instruction que de l'amener, le processus de transport de l'instruction continue et amène l'instruction suivante après le 4e cycle. Donc en tout, cette suite de commandes prend 11 + 8 = 19 cycles lorsque la file So in all, this set of command takes 11 + 8 = 19 cycles when the prefetch queue is empty. Consider the case where we reverse the operation. We do the push first and then the load. In such a case, we use 11 cycles to run the push and only 7 cycles for the load. That's 18 in total.

So did Zilog screw-up when counting their instruction speeds? Not at all, as you can see, if the prefetch queue is empty, then you're going to have to count knowing that 4cycles are needed for the fetch. If the prefetch is not empty, then considering that all the bytes needed for the instruction has been loaded, you can count Zilog's cycles.

Il y a aussi un autre effet du BUS que vous devez prendre en considération. Considérons le cas "ld a, (hl)". Cette commande fait 1 octet de long, et selon Zilog, prend 7 cycles. Mais à cause de notre bon ami le BUS de données, elle prend en fait 8 cycles, et voici pourquoi. The reason is the following, it takes 4 cycles to prefetch the instruction like we saw, but it also takes 4 cycles to access the memory in (hl) [BUS]. So in total you're counting 8 cycles. Of course if you already have the instruction in the queue. Then you're really looking at 7cycles as usual.

Il ne s'agit pas ici de comprendre le comptage de cycles, car cela ne vous donnera pas du tout un bon aperçu de la vitesse du programme. Il y a beaucoup plus de facteurs qui peuvent affecter la performance d'un programme sans compter le bus lui-même. Pour connaître la vitesse réelle d'un programme, vous ferez toujours mieuz de crééer un compteur et de l'utiliser. Au moins il vous donnera une vraie comparaison de la vitesse relative. Last notes on the prefetch queue. Note that as soon as you jump somewhere, you will empty your queue.

 

Code auto-modifiant         Self Modifying Code

Le caméléon est un animal très curieux. Il est plutôt bien connu pour sa capacité à changer de couleur en fonction de celle de son environnement. C'est en fait un de ses systèmes de défense. Si vous regardez de loin et qu'il se cache, vous remarquerez qu'il s'adapte très bien à son environnement en adoptant sa coloration. Prenez le même caméléon et placez-le dans un environnement différent, et vous le verrez changer encore une fois de couleur pour qu'elle corresponde à l'arrière-plan. Le code auto-modifiant fait pratiquement la même chose, si bien entendu vous avez préalablement programmé les conformations qu'il peut adopter. C'est une très chouette technique qui peut faire des merveilles dans un programme si elle est bien appliquée. Mais elle comporte aussi un piège (important) (bad trap) si le programmeur n'est pas complètement renseigné à propos d'elle.

Prenons le premier cas. Vous n'avez plus de registres et vous avez besoin de quelque chose qui puisse stocker une valeur; à ce moment, la plupart des programmeurs opteront pour la création d'une variable typique. Mais bien que cette méthode soit acceptable, si vous faites référence à cette valeur plusieurs fois, alors vous réduisez la vitesse de votre code. La solution ? Utilisez la variable déjà incluse dans votre code, comme ça vous n'aurez besoin d'enregistrer la valeur qu'une fois donc de n'accéder à la mémoire indirecte qu'une fois au lieu de deux. (Enregistrement et rappel). Comment y arriver ? Prenons la partie de code suivante

ld (VarName), a
;(Faire quelques trucs)
ld a, (VarName)

Au lieu de l'écrire comme ça, réduisez la dernière instruction en utilisant à la place un chargement direct en mémoire comme dans

ld (Patch1 + 1), a
;(Faire quelques trucs)
Patch1: ld a, 12h


Notez bien que la valeur 12h n'affecte rien. Quoi que vous mettiez ici, cela ne changera rien puisque ça sera réécrit. Notez aussi que cette technique utilise moins de cycles que push/pop.

Maintenant supposez un autre cas: vous avez deux fonctions, la première efface l'écran du haut vers le bas, et la seconde du bas vers le haut.

Start:ld a, (hl)
      ; (Faire quelques opérations sur "a")
      ld (bc), a

Patch1:
      inc hl
      inc bc
      dec d
      jr nz, Start

La seule chose que vous avez à faire est de "patcher" (to patch) le mot de valeur "0323h" à Patch1 pour incrémenter et "patcher" le mot de valeur "0B2Bh" pour décrémenter. Vous pouvez obtenir ces valeurs du fichier .TAB que vous utilisez pour compiler vos fichier assembleur z80. Ici, si les opérations sur a sont assez longues, vous pouvez gagner quelques octets.

Vous devez vous assurer que quoi que vous ayez changé     You have to make sure that whatever you modified isn't currently in the prefetch queue. Or else that means the changes simply won't seem to have applied. There's a great potential for this technique.

 

Math VS Tables

Il y a quelques jours, je travaillais sur l'écran d'entrée d'un petit jeu. Je voulais une ligne horizontale qui alternerait les couleurs tous les 3 pixels. Comme chaque pixel est représenté par 1 bit chez le Z80, ce n'est pas chose facile. J'ai une belle longue formule qui marchait très bien, mais elle contenait beaucoup de commandes "shift" et similaires. La version finale est en fait une simple chaîne de 3 octets affichée à l'écran. Elle n'utilise que les commandes de copie et ne nécessite pas du tout d'opérations. Pas besoin de dire qu'aucune équation n'arriverait au même résultat en 3 octets. Donc dans ce cas, l'utilisation de tableaux pour enregistrer l'image était la meilleure idée. Toute cette idée d'utiliser ce motif était aussi une optimisation en soi, vu que tout l'écran utilisait ces motifs. En enlevant l'image de 1Ko et en la remplaçant par des opérations mathématiques, j'ai pu réduire la taille de 1Ko à environ 0,3Ko, ce qui est une amélioration assez intéressante du point de vue de la taille. (L'écran de démarrage a été fait pour le jeu The bomb, en passant)

Donc lorsque vous tombez sur un motif, réfléchissez bien s'il est préférable d'utiliser une équation, un tableau simple ou une combinaison des deux comme j'ai utilisé ici. Vous pouvez calculer un tableau dans un programme si vous constatez que cela prend moins de place que de l'enregistrer directement.

 

Unrolled Loops

Jadis, il y avait des compétitions de marins. Il y en avait différents types, mais celui qui m'intéresse le plus consiste au transport de poids. Pour cette épreuve, le marin avais un chariot plein de choses lourdes toutes en rapport avec la navigation. Il y avait quelques ancres lourdes, des chaînes, des morceaux de bois et plus encore. Le gagnant était celui qui arrivait à charger le second chariot en mettant le moins de temps possible. Le premier prit un objet à la fois et le transporta à l'autre chariot très rapidement et fit un bon temps. Le second marin prit deux objets à la fois, réduisit donc son temps de marche, mais aussi sa vitesse de déplacement vu que le poids était supérieur. Le dernier, aussi gagnant pour le meilleur temps, prit tous les objets sur son dos et marcha à vitesse de tortue, mais n'a pas eu à revenir du tout au premier chariot.

C'est exactement l'effet d'un   unrolled loop.  Il s'agit de prendre la boucle typique "FOR/DO/WHILE" et de la diviser pour loop and split it so that you do more then one of the same thing in it, but reducing the number of iterations. This will increase the size of your code obviously, but it will also speed it up. Here's why. Consider the speed of a jump. It's around 10/12 cycles. And now that of a comparison which is about 4cycles. Suppose you do that loop 100 times, that means you're wasting about 16cycles per iteration therefore 1600cycles for the whole loop. That's a great amount. If you would copy/paste the internal loop only once, you'd need about 50iterations instead. Then you cut your looping cycling speed by about half. Which is 800cycles. If your loop is small enough, you can ever consider unrolling the whole loop which means you don't even need to do a check at all.

      ld b, 100d
Loop: add hl, de
      djnz Loop ; Nous gaspillons environ 13x100 = 1300cycles

      Ld b, 50d
Loop: add hl, de
      add hl, de
      djnz Loop ; Ici nous utilisons 13x50 = 750cycles

 

Smart direct RAM usage

Il y a quelques années, les PC n'avaient pas beaucoup de RAM et les exigences minimum de mémoire pour un jeux étaient particulièrement élevée pour certains d'entre eux. Du temps de DOS, on ne pouvait adresser directement qu'un demi mégaoctet. Certains jeux demandaient des quantités énormes de mémoire comme 620Ko pour fonctionner. Cela signifie que vous n'aviez que 20Ko pour charger vos pilotes. Il était vraiment difficile de tout serrer pour que ça tienne. C'est en voyant cela que la plupart des gens on compris la bêtise de l'affirmation de Bill Gates "640Ko devraient suffire pour tout le monde". Et puis regardez qui consomme 25 fois cette valeur pour un OS, hein ? =) Mais de toute façon, au fur et à mesure que le PC devenait plus connu, quelques personnes ingénieuses trouvèrent des "trous" libres dans la mémoire qui pouvaient être utilisés pour exécuter des programmes. On pouvait trouver par exemple le trou du Mode texte noir (B000-B7FF), et dépendamment de votre PC, d'autres trous pouvaient être utilisés. Intel, Lotus et Microsoft ont aussi développé un programme nommé "HIMEM.SYS" qui rendait la mémoire accessible à DOS. Ils ont aussi trouvé une fuite qui permettait d'utiliser la première partie de votre RAM comme mémoire conventionnelle par l'utilisation de calls d'adressage non-standard. Aussi connu sous l'acronyme de HMA.

Pas besoin de continuer tout le long pour que vous compreniez où je veux en venir. La calculatrice comporte aussi des trous. Tous ne sont pas utilisés ni ne doivent être gardés. Il y a plusieurs trous que vous pouvez utiliser comme des variables temporaires, et vous devriez savoir où ils se trouvent si besoin est. Pour les débutants, à l'adresse $4000, il y a un bon 16Ko réservé pour la ROM. Cette page est utilisée par la calculatrice pour exécuter tous les ROM calls que vous écrivez. Si vous n'utilisez pas un quelconque ROM call dans votre code, pourquoi ne pas faire un meilleur usage de cet espace ? Assurez-vous seulement de recharger la page originale lorsque vous avez fini. Vous pouvez obtenir la page courante en utilisant la commande "in a, (5)". À la suite de cette mémoire tampon, à $8000, il y a un autre tampon de 16Ko réservé pour   RAM flipping.  Donc si vous accédez à quelque chose en-dehors de la mémoire directe, il se retrouve ici et devient lisible. Encore une fois, si vous n'utilisez pas cet espace (et la plupart des programmes n'ont pas vraiment besoin de l'utiliser), alors utilisez-le pour des sprites ou des données du genre.

Le plus gros problème est probablement de savoir comment amener les données extérieures vers cette page de la RAM pour que vous les utilisiez. Eh ben, le concept est un peu délicat à comprendre, mais marche plutôt bien une fois appliqué. Premièrement, obtenez l'adresse à 3 octets de votre variable via la VAT. Consultez d'autres références à ce propos. Une fois cette adresse obtenue, calculez à quelle page se trouvent ces données. C'est un calcul vraiment simple, "Page = (Adresse / 16K) - 3". Assurez-vous de charger cette page dans la zone de ROM ($4000) et aussi, de charger la page suivante dans la zone ($8000). Donc si vos données sont dans la limite de 16Ko, elles devraient toujours être là! Maintenant, ce qu'il est probable qui vous intéresserait de savoir, c'est comment libérer la page de ROM et avoir uniquement les données dont vous avez besoin dans la page de RAM. Pour faire ça, vous devez obtenir l'adresse de départ par la formule (Adresse AND (16K-1)). Ensuite, copiez ce tampon à $8000. Mais vous devez le copier de la fin vers le début, car si vous le copiez de manière normale du début vers la fin, il y a un risque que vous écrasiez une partie des données dont vous avez besoin dans la zone $8000. Une fois indiqué cette mise en garde, voici le code pour faire ces opérations: (en considérant que ex pointe vers les données elles-mêmes)

in a, (5)
ex af, af'

ld a, (ix + 2)
ld l, (ix + 1)
sla l
rla
sla l
rla ; a = Page de données (This is AHL/16K)

add a, %01000000 - 3 ; Accéder à la page de RAM - 3
out (5), a ; Afficher dans $4000
inc a
out (6), a ; Les données sont entre $4000 et $C000

ld bc, Length
ld a, (ix + 1)
and %00111111
ld h, a
ld l, (ix)
ld de, $4000 + 1 + Length
add hl, de
ld de, $BFFF
lddr ; Enregistrer les données dans la page de swap RAM ($8000)

ex af, af'
out (5), a ; Revenir à la page de ROM

Notez qu'en fait nous sommes ici en train de copier les données, elles finissent donc à $BFFF. Elles ne doivent pas obligatoirement se trouver là, c'est juste une préférence personnelle !

Pour ceux qui travaillent avec des niveaux de gris, vous pouvez aussi utiliser les trous du système pour mettre les écrans virtuels. Il y a aussi un beau trou à $CA00. Celui-là est couramment utilisé par les programmeurs de niveaux de gris. Since it has a nice screen-length buffer free. For temporary variables, you may also want to check at $C100. There's a wealth of space all around the calculator, check in your .INC/.H files and see what addresses aren't in use, try to see if they don't affect anything, if not, then you can use them =) After all, the most we're getting out of this calculator, the better it is. Also you might want to check if you can use places where you don't use or need the variables/ROM calls. Just make sure that you give back it's values after if they're important at all for the calculator to work well.

Note: Les trous expliqués ici ne sont présents que sur TI-86 à ma connaissance, peut-être y sont-ils sur d'autres aussi ?

 

Branched Jump Size Optimization

Cette technique sera très utile aux codeurs qui ont optimisé la vitesse au MAXIMUM. L'idée est simple, vous pouvez économiser 1 octet si vous utilisez "jr" au lieu de "jp". Supposons le cas où vous voudriez aller à une adresse qui a un delta > 127. En d'autres mots, une instruction "jp" semble la seule option utilisable ici ! Pas tout à fait. Si quelque part dans votre code, vous avez un jump qui saute là où vous le voulez, il ne reste plus qu'à faire un jump vers celui-ci. Now that might sound a tad cryptic at first, but here's an example. Cependant, notez bien que cette méthode n'optimise pas réellement en faveur de la vitesse, car elle requiert 2 jumps ou plus pour atteindre la position désirée.

Start:
; Some core goes here!
Jp1: jr Start
; Beaucoup de code va ici
Jp2: jr Jp1
; Beaucoup plus de code va là (Il y a plus que 127 octets entre Jp2
; et Start, c'est pourquoi nous sauterons à Jp1 qui sautera à son tour à Start.
; Nous économisons un octet ici !

jr Jp2

 

Travailler avec des registres 16 bits peut signifier 8 bits

Voici un truc que la plupart des garages pourraient utiliser. Si vous allez les voir et qu'ils trouvent un problème avec votre alternateur par exemple, ils changent tout le morceau, habituellement. Au lieu de ça, ils pourraient essayer d'isoler exactement ce qui ne va pas dans l'alternateur et changer juste cette pièce. Bien sûr, eux sont là pour gagner du temps et de l'argent, mais nous nous sommes ici pour rendre nos jeux meilleurs et plus rapides, donc nous utiliserons les techniques pour changer uniquement ce qui est nécessaire, et pas le reste.

Plusieurs fois, vous travaillez avec un registre 16 bits; peut-être pour adresser quelque chose en mémoire ? Vous utilisez une fonction d'incrémentation. Ça fait 6 cycles ! Vous pourriez incrémenter uniquement la partie basse si vous êtes sûr qu'en incrémentant, vous n'atteindrez pas la limite de 256 octets. Cela ne prendrait alors que 4 cycles. Si vous placez vos données en mémoire de manière réfléchie, vous pouvez vous assurer que vous ne dépassez pas une telle limite et donc vous pouvez utiliser une incrémentation de 8 bits. Une autre application de ceci peut se faire pour l'addition. Parfois vous avez plusieurs "couches"      (layers)    à arranger (fixup)   et vous voulez actualiser les deux en même temps (pour les niveaux de gris, par exemple). Vous avez donc besoin d'écrire votre octet à (hl), puis ensuite à (hl + Page2 - Page1). Bien sûr la plupart des codeurs utiliseraient "add hl, bc", b contenant le delta de la page. Un moyen plus fin de faire ça consiste à incrémenter seulement "h" en laissant "l" tel quel. Donc finalement, vous n'avez besoin que d'une instruction "add h, N" qui se fait en 7[8]cycles au lieu de quelque chose aux environs de 21[23]cycles pour charger le delta et l'additionner. And BTW, you also get to free up the temporary addition register =). Mais ici encore, vous devez vous assurer que vos pages se terminent dans une limite de 256 octets dans la mémoire.

 

Afterwards

Eh bien, on dirait que c'est tout ce qui me revient comme techniques d'optimisation pour le moment... Il y en a sans aucun doute beaucoup plus que celles que j'ai mentionné ici, mais elles feront le sujet peut-être d'un prochain article plus tard. Je n'ai pas écrit non plus les techniques évidentes d'optimisation. Celles comme "xor a" au lieu de "ld a,0", étant donné que je les considère évidentes et non réellement du niveau de programmeurs intermédiaires ou avancés =). En d'autres mots, je considère que vous connaissez déjà ce genre de choses.

Vous avez aimé ce tuto, vous en voulez plus, avez des suggestions ? Corrections, bugs dans le code, quoi que ce soit ? Écrivez-moi: Ti_chris@yahoo.com

 

    Cliquez pour revenir au menu du site... ou ici pour retourner au menu du tutorial.