Techniques de programmation en assembleur |
Article posté par geogeo Techniques d'optimisations en assembleur
Nous supposons que vous maîtrisez l'assembleur 68000 et que les instructions qui figurent dans cet article sont parfaitement connues.
Même si les performances des langages de haut niveau permettent, dans bien des cas, de se dispenser d'une phase de programmation en langage assembleur, certaines fonctionnalités rendent incontournable son utilisation qui seule peut assurer à la fois la rapidité, la concision et la précision réclamées dans ces circonstances précises. Même si l'économie de quelques microsecondes ou d'octets peut sembler dérisoire dans de nombreux cas, certaines circonstances, qui sont spécifiquement celles qui ont conduit à l'utilisation du langage assembleur, nécessitant la prise la prise en compte de ce besoin (routines d'interruption, d'entrées-sorties, programmation d'un système d'exploitation, routines graphiques, etc.). De toute façon, la recherche d'un rendement optimum du code programmé, si elle n'est pas toujours nécessaire, est une très bonne discipline, qui apporte de surcroît la satisfaction d'un résultat efficace.
Le jeu très varié du MC68000 (56 types d'instruction) permet une programmation simple des algorithmes même les plus complexes. Cependant, dans le but d'augmenter l'efficacité d'exécution du code dans un certain nombre de cas, plusieurs instructions ont été incluses dans le jeu. Leur examen attentif permet de découvrir quelques subtilités de programmation qui peuvent se révéler précieuses en plus d'une occasion.
Nous vous proposons ainsi d'étudier dans le détail quelques possibilités d'optimisation, en nous attachant tout d'abord aux instructions proprement dites avant d'examiner les structures de contrôle. Il est bien évident que plusieurs cas le code optimisé sera obligatoirement moins « lisible » qu'un code plus simplifié, c'est pourquoi il est préférable, au moins dans un premier temps, d'indiquer clairement en commentaire du programme la justification de l'emploi d'une solution qui peut apparaître un peu obscure.
Dans nos exemples, la version [a] expose le code simplifié, et [b] le code optimisé. Les symboles suivants ont été utilisés :
Les temps d'exécution en cycles machine puis la taille en octets sont indiqués entre crochets pour chaque instruction. Le signe ~ indique que les valeurs ne prennent pas en compte le temps nécessaire pour effectuer le calcul de l'adresse effective ainsi que les mots d'extensions servant à préciser l'adresse des opérandes.
Entrons dans le vif su sujet.
Transferts de données
L'utilisation de l'instruction moveq qui permet de charger un registre entier (les 32 bits sont affectés) avec une valeur comprise entre -128 et 127, peut être étendue de plusieurs façon : [1a] clr.l Dn [6 :2]
[1b] moveq #0,Dn [4 :2]
Cette optimisation se passe de commentaires ! Inutile de perdre 2 cycles.
127 < DATA <256
[2a] move.l #DATA,Dn [12:6]
[2b] moveq #DATA-128,Dn [4:2] tas Dn [4:2] ou not.b Dn [4:2] ou neg.b Dn [4:2] Total [8 :4]
Bien que l'instruction tas (test and set) soit destinée primitivement à positionner un bit sémaphore (dans le cas d'un système d'exploitation multitâche), son utilisation dans ce cas permet, par la mise à 1 du bit 7 du registre Dn, d'augmenter la première valeur chargée dans le registre Dn de 128. Le gain est double : en temps d'exécution et en taille d'instruction.
[3a] move.l #255,Dn [12:6] move.w #255,Dn [8 :4]
[3b] st Dn [6 :2]
A condition de ne pas tenir compte des octets supérieurs du
registre. Sinon utilisez les instructions précédentes. -256 < DATA < -128
[4a] move.l #DATA,Dn [12:6]
[4b] moveq #-256-DATA,Dn [4:2] neg.b Dn [4:2] Total [8 :4]
-65536 < DATA < -65407
[5a] move.l #DATA,Dn [12:6]
[5b] moveq #-65536-DATA,Dn [4:2] neg.w Dn [4:2] Total [8 :4]
Deux usages de la négation sur un octet ou sur un mot. Le second cas est toutefois d'une utilisation beaucoup moins fréquente dans les programmes.
[6a] move.l #$FFFFFF00,Dn [12:6]
[6b] moveq #-1,Dn (-1 = #$FFFFFFFF)[4:2] not.b Dn [4:2] Total [8 :4]
[7a] move.l #$000000FF,Dn [12:6]
[7b] moveq #0,Dn [4:2] not.b Dn [4:2] Total [8 :4]
[8a] move.l #$FFFF0000,Dn [12:6]
[8b] moveq #-1,Dn [4:2] not.w Dn [4:2] Total [8 :4]
[9a] move.l #$0000FFFF,Dn [12:6]
[9b] moveq #0,Dn [4:2] not.w Dn [4:2] Total [8 :4]
Pour les secondes instructions [b] et [8b] on aurait bien entendu pu tout aussi bien employer l'instruction clr.b et clr.w. A chacun de choisir.
On peut de la même manière obtenir, par exemple des valeurs de masque plus complexes :
[10a] move.l #$FFFFFF0F,Dn
[10b] moveq #-16,Dn (-16 = #$FFFFFFF0) neg.b Dn ou [11a] move.l #$FFFF000F,Dn
[11b] moveq #-16,Dn neg.w Dn
Et ainsi de suite...
[12a] move.l #$FFF0FFFF,Dn [12:6]
[12b] moveq #-16,Dn [4:2] swap Dn [4:2] Total [8 :4]
[13a] move.l #$000F0000,Dn [12:6]
[13b] moveq #16,Dn (16 = #$0000000F)[4:2] swap Dn [4:2] Total [8:4]
Tout comme dans les exemples précédents, d'autres possibilités peuvent être utilisées.
-129 < DATA < +128
[14a] move.l #DATA,
[14b] moveq #DATA,Dn [4:2] move.l Dn, Total [12~:4~]
Cette codification est particulièrement utile pour l'empilage de paramètres, comme dans l'appel de certaines fonctions du système où ceux-ci peuvent être équivalentes à -1 ou 0. Par exemple : moveq #-1,D0 [4 :2] move.l D0,-(SP) [12 :2] moveq #0,D0 [4 :2] move.l D0,-(SP) [12 :2] Total [32 :8]
Au lieu de : move.l #-1,-(SP) [20 :6] clr.l -(SP) [22 :4] Total [42 :10]
An = adresse impaire
[15a] move.b (An)+,Dn [8:2] lsl.w #8,Dn [22:2] move.b (An),Dn [8:2] Total [38:6]
[15b] move.b (An)+,-(SP) [12:2] move.w (SP)+,Dn [8:2] move.b (An),Dn [8:2] Total [28:6]
En présence du problème du transfert d'un mot à partir d'une adresse impaire, la décrémentation automatique d'un mot et non pas d'un octet du registre SP (pile système) permet de transférer directement l'octet provenant de An dans l'octet supérieur du mot empilé. Cet octet sera donc à sa place dans Dn lors du dépilement. Bien entendu pour une adresse paire le problème est immédiatement résolu !
[16a] move.b #WW,(An) [16:4] move.b #XX,1(An) [16:4] move.b #YY,2(An) [16:4] move.b #ZZ,3(An) [16:4] Total [64:16]
[16b] move.l #WWXXYYZZ,(An) [12:6]
movep est particulièrement destinée à l'interfaçage des microprocesseurs 8 bits mais peut for bien servir en beaucoup d'autres occasions à ne pas négliger. Elle opère également sur une taille d'un mot et dans le sens mémoire->registre. Son étude attentive peut être très profitable. Mais ne fonctionne dans le cas d'un système avec périphériques, ce qui n'est pas le cas des TI68K.
Transferts d'adresses
[17a] movea.l #0,An [12:6]
[17b] suba.l An,An [8 :2]
C'est évident !
8 < DATA < $0000FFFF ou $FFFF0000 < DATA < $FFFFFFF8
[18a] adda.l #DATA,An [14:6] ou [19a] suba.l #DATA,An [14:6]
[18b] lea.l DATA(An),An [8 :4] ou [19b] lea.l -DATA(An),An [8 :4]
Cette instruction, l'une des moins bien comprises dans le jeu du MC68000, devrait notamment être employée lors des opérations de rectification de pointeur de pile qui interviennent après un appel à une routine. Quand 0 < DATA < 9, on emploi alors la version addq ou subq des instructions add et sub qui s'exécutent dans le même temps que l'instruction lea, mais ont une taille inférieure de 2 octets.
[20a] move.l #Adresse,-(SP) [20 :8] move.l #Adresse+4,-(SP) [20 :6] move.l #Adresse+8,-(SP) [20 :6] ...etc Total [60 :18]
[20b] lea.l Adresse,An [12:6] pea (An)+ [12:2] pea (An)+ [12:2] pea (An) [12:2] ...etc Total [48:12]
L'instruction pea fonctionne exactement de la même façon que l'instruction lea. Toutes les deux n'admettent qu'une seule taille d'opérande, sur un mot long. Dans [18], le gain est indépendant du déplacement (à condition de ne pas oublier que ce dernier est codé sur au plus 16 bits), et s'accroît avec le nombre de paramètres empilés.
Opérations logiques et arithmétiques
[21a] andi.l #$0000FFFF,Dn [16:6]
[21b] swap Dn [4:2] clr.w Dn [4:2] Swap Dn [4:2] Total [12:6]
Comme quoi 3 instructions peuvent parfois être plus efficaces qu'une seule !
-127 < DATA < +128
[22a] addi.l #DATA,
[22b] moveq #DATA,Dn [4:2] add.l Dn, Total [16~:4~]
On peut utiliser le même mécanisme pour les autres instructions immédiates, à savoir: subi.l andi.l eori.l ori.l cmpi.l
DATA = 2 puissance k
[23a] cmpi.l #DATA,Dn [14 :6]
[23b] btst.l #k,Dn [10:4]
Si le bit k de Dn est à 1, la valeur de Dn est supérieurs ou égale à DATA. Par exemple : btst.l #16,D0 [10 :4] beq.s PLUS_LOIN [10 :2] moveq #1,D0 [4 :2] swap D0 [4 :2] PLUS_LOIN : move.l D0,D1 [4 :2] Total [32 :12] Au lieu de : cmpi.l #$10000,D0 (#$10000 = 65536) [14 :6] bmi.s PLUS_LOIN [10 :2] move.l #$10000,D1 [12 :6] PLUS_LOIN : move.l D1,D2 [4 :2] Total [40 :16]
0 < DATA < 8
[24a] moveq #DATA+8,Dn rol.w Dn,Dm [22+DATA*2:2] (1a)
[24b] moveq #8-DATA,Dn ror.w Dn,Dm [6+((8-DATA)*2):2] (1b)
0 < DATA < 16
[24a] moveq #DATA+16,Dn rol.l Dn,Dm [32+DATA*2:2] (2a)
[24b] moveq #16-DATA,Dn ror.l Dn,Dm [8+((16-DATA)*2):2] (2b)
On peut éviter les rotations importantes grâce à cette technique. Si, par exemple, dans [21] DATA-4, c'est-à-dire que l'on souhaite obtenir une rotation de 4+8, soit 12 bits. (1a) sera égal à 30, et (1b) à 14. La différence est notable ! Dans le cas d'une rotation sur un mot on économise donc 16 cycles machine, et sur un mot long, 32 cycles.
0 < DATA < 16
[26a] moveq #DATA+16,Dn lsr.l Dn,Dm [40+(DATA*2) :2]
[26b] moveq #DATA,Dn clr.w Dm [4:2] swap Dm [4:2] lsr.w Dm [6+(DATA*2):2] Total [14+(DATA*2):6]
[27a] moveq #DATA+16,Dn lsl.l Dn,Dm [40+(DATA*2):2]
[27b] moveq #DATA,Dn lsl.w Dm [6+(DATA*2):2] swap Dm [4:2] clr.w Dm Total [14+(DATA*2):6]
[28a] moveq #DATA+16,Dn asr.l Dn,Dm [40+(DATA*2):2]
[28b] moveq #DATA,Dn swap Dm [4:2] ext.l Dm [4:2] asr.l Dn,Dm Total [14+(DATA*2):6]
Tout comme les rotations, les décalages importants peuvent être accélérés efficacement, en sacrifiant un peu la taille du code cette fois. Le décalage arithmétique à gauche (asl) s'effectue comme un décalage logique.
Multiplications et divisions rapides
Ces deux opérations sont les moins rapides des instructions
courantes. Il peut être avantageux dans des circonstances bien précises
d'utiliser des algorithmes de remplacement qui se révèlent plus rapides, au
détriment toutefois de la taille du code. Le cas d'une multiplication par une
donnée immédiate facilement décomposable en sommes de puissances de 2 en est un
des exemples caractéristiques.
[29b] move.l Dn,Dm [4:2] lsl.l #2,Dm [12:2] add.l Dm,Dn [8:2] Total [24:6]
[30a] mulu #20,Dn [44:4]
[30b] lsl.l #2,Dn [12:2] move.l Dn,Dm [4:2] lsl.l #2,Dm [12:2] add.l Dm,Dn [8:2] Total [36:8]
Dans le second cas, le premier décalage multiplie la valeur initiale de Dn par 4. Le second décalage la multiplie par 16. La somme équivaut donc à 16+4 et donne donc le même résultat qu'une multiplication par 20. il n'est pas inutile de rappeler q'un décalage d'un bit sur la gauche équivaut à une multiplication par 2 et que dans ce cas, une simple addition est que rapide que le décalage. Par exemple : add Dn,Dn [4 :2] au lieu de : lsl.w #1,Dn [8 :2]
Si on souhaite effectuer une multiplication signée (muls), on emploiera alors un décalage arithmétique (asl), et un test du bit V du CCR permettra de connaître le signe du résultat.
Pour une division dont le quotient attendu est inférieur à 6, on peut employer l'algorithme suivant, dans lequel le diviseur est contenu dans Dn, le diviseur dans Dm. A la sortie de la boucle, le quotient se trouve dans Dr et le reste dans Dm :
[31] moveq #-1,Dr [4 :2] sub.w Dn,Dm [4 :2] DIVISE_1 : sub.w Dn,Dm [4 :2] dbmi.w Dr,DIVISE_1 [10:4] ou [12 :2] neg.w Dr [4 :2] add.w Dn,Dm [4 :2] Total [16+14*quotient+2:14) Le temps d'exécution varie entre 18 cycles (quotient = 0) et 130 cycles (quotient = 8) alors que l'instruction équivalente divu Dn,Dm demande entre 126 et 140 cycles. Cet algorithme divise les 16 bits de poids faible de Dm par
les 16 bits de poids faible de Dn. Pour réaliser une division sur 32 bits, il
faut modifier les opérations portant sur Dn et Dm pour les faire porter sur des
longword, et dans ce cas, le temps d'exécution est de 24+18*quotient+2, donc il
ne devient intéressant que pour des quotients <= 5.
Dans le même ordre d'idées, le calcul d'un modulo peut s'effectuer ainsi :
3 < DATA1 < $3FFFF (modulo recherché) DATA2 = (puissance de 2)-1 immédiatement supérieure à DATA1. Par exemple si DATA1 = 5 (%101), DATA2 = 7 (%111).
[32a] divu #DATA,Dn [140 :4] swap Dn [4:2] move Dn,Dm [4:2] Total [148:8]
[32b] move #DATA,Dm [8:4] and Dn,Dm [4:2] cmp Dn,Dm [4:2] bmi.s VAL_MODULO [10:2] sub Dn,Dm [4:2] VAL_MODULO: Total [26 ou 28:12]
La valeur de Dn modulo DATA1 se trouve dans Dm. Et si DATA1 < 128, on gagne encore 4 cycles en utilisant moveq dans [32b]. De plus l'opération peut être effectuée sur un mot long sans difficulté.
La suite prochainement
Source : ST Magazine numéro 17 et 18 par Daniel Fournier, modifié par Geoffrey Anneheim, copyright Ti-Gen 2005. |
>> Vos commentaires [20]
|