Obligement - L'Amiga au maximum

Lundi 19 novembre 2018 - 03:50  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

 · Accueil
 · A Propos
 · Articles
 · Galeries
 · Glossaire
 · Liens
 · Liste jeux Amiga
 · Quizz
 · Téléchargements
 · Trucs et astuces


Articles

 · Actualité (récente)
 · Actualité (archive)
 · Comparatifs
 · Dossiers
 · Entrevues
 · Matériel (tests)
 · Matériel (bidouilles)
 · Points de vue
 · En pratique
 · Programmation
 · Reportages
 · Tests de jeux
 · Tests de logiciels
 · Tests de compilations
 · Articles divers

 · Articles in english
 · Articles en d'autres langues


Twitter

Suivez-nous sur Twitter




Liens

 · Sites de téléchargements
 · Associations
 · Pages Personnelles
 · Matériel
 · Réparateurs
 · Revendeurs
 · Presse et médias
 · Programmation
 · Logiciels
 · Jeux
 · Scène démo
 · Divers


Jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Partenaires

Annuaire Amiga

Amedia Computer

Relec

Hit Parade


Contact

David Brunet

Courriel

 


Programmation : Assembleur - L'art du langage machine
(Article écrit par John Toebes et extrait d'Amiga News Tech - octobre 1989)


Note : traduction par Stéphane Schreiber.

Il est bien connu que pour réaliser des routines requérant une grande vitesse d'exécution, rien ne vaut l'assembleur. En fait, il serait plus juste de dire que quel que soit le langage utilisé, l'important est de bien l'utiliser.

Un programmeur en langage machine averti, peut très rapidement distinguer trois types de programmes :
  • Les très bien écrits.
  • Les bien écrits.
  • Les inqualifiables !
Il n'y a pas de règles absolues qui permettent de distinguer ces trois types de programmation, seulement des "lignes de conduite". Bien qu'elles tendent à varier d'un programmeur à un autre, si le code est bon ou excellent, personne ne viendra dire le contraire. Le but de cet article est de vous aider à écrire des programmes que l'on appréciera à leur juste valeur.

De l'excellent code se reconnaît très facilement ; il a tendance à utiliser presque tous les trucs connus, voire à en inventer de nouveaux. Sa qualité essentielle est... de faire ce pour quoi on l'a écrit !

Du bon code est plus facile à comprendre pour le débutant suivant pour ainsi dire à la lettre les règles connues de la programmation en assembleur. Il n'est malheureusement pas toujours optimisé, mais présente l'avantage de fonctionner.

Reste le code "inqualifiable", qui est... heu... disons pas très joli à voir.

La marche à suivre pour obtenir un code optimal peut-être divisée en trois étapes :
  • Optimisation "ligne à ligne".
  • Optimisation locale (sous-programmes, etc.).
  • Optimisation globale.
En fait, ces trois étapes peuvent paraître à première vue identiques. Il est vrai qu'elles ne diffèrent que très peu.

L'optimisation "ligne à ligne"

Le jeu d'instructions du MC68000 n'est peut-être pas très vaste, mais ses nombreux modes d'adressage autorisent une programmation plus qu'optimisée, aussi bien en temps d'exécution qu'en taille de programme (par la suite, nous ne nous référons plus qu'au 68000 en oubliant les 68020 et 68030, car c'est à l'heure actuelle le processeur le plus répandu). Les premières règles à observer lors du choix des instructions sont :
  • Éviter les valeurs immédiates sur 32 bits.
  • Éviter les valeurs 32 bits avec les registres d'adresse.
  • Éviter l'adressage absolu en mode 32 bits.
  • Éviter les instructions MUL et DIV.
  • Utiliser si possible la forme QUICK des instructions.
  • Oublier que l'instruction CLR existe !
Illustrons un peu ces règles. Vous trouverez ci-dessous, numérotés de A à E, quelques exemples simples d'optimisation.

Assembleur

Ces quelques lignes seraient plus efficaces écrites comme suit ;

Assembleur

Dans l'exemple D, nous avons doublé la taille du code, mais réduit de moitié le temps d'exécution. Cette augmentation de taille (4 octets) est suffisamment négligeable pour préférer cette solution.

L'exemple C est une parfaite illustration d'une règle amusante de l'assembleur, qui veut que deux instructions s'exécutent parfois plus rapidement qu'une seule, tout en prenant moins de place en mémoire !

Ce qui amène une septième règle importante : si vous pouvez utiliser MOVEQ pour des constantes 32 bits, faites-le ! Cette règle s'applique à toute instruction dans laquelle il est possible d'utiliser un registre de donnée annexe. Par exemple :

Assembleur

...serait beaucoup plus efficace écrit ainsi :

Assembleur

Bien sûr, il n'est pas obligatoire d'utiliser à chaque fois un registre. Il est toujours préférable, pour de petites valeurs, d'utiliser la variante QUICK des instructions. Bien que l'on puisse également ruser de ce côté-là :

Assembleur

...sera avantageusement remplacé par :

Assembleur

Une autre utilisation courante des constantes se retrouve dans les manipulations de la pile. Lorsqu'on appelle une routine en langage compilé, par exemple en C, qui requiert des paramètres sur la pile, il faut généralement la remettre "dans l'état où on l'avait trouvée en entrant". Ce qui ressemble souvent à :

Assembleur

Pour une correction de 8 octets ou moins, il vaut mieux utiliser :

Assembleur

Mais pour neuf octets ou plus, l'instruction LEA est tout à fait appropriée :

Assembleur

Il ne faut pas se limiter à la simple instruction MOVEQ pour charger une constante dans un registre. Tout dépend bien sûr de la constante elle-même, mais il y a plusieurs astuces que l'on peut utiliser. Par exemple, pour charger une valeur de $10000 dans un registre, la première idée qui vient à l'esprit est :

Assembleur

...alors qu'il est cent fois mieux d'écrire :

Assembleur

Cela marche pour une valeur comprise entre $FF800000 et $007F0000, où le mot de poids faible est à zéro.

Une autre astuce du même genre, lorsqu'on désire initialiser un registre avec une valeur trop grande pour l'instructeur MOVEQ :

Assembleur

Le couple d'instructions MOVEQ/NEG.B permet de charger n'importe quelle valeur 8 bits dans un registre de données. Ceci peut-être étendu à MOVEQ/NEG.W pour des constantes comprises dans le domaine $FFFF00xx à $0000FFxx.

Pour initialiser des constantes comprises entre $80000000 à $01000000, les instructions ROL et ROR offrent une alernative interessante :

Assembleur

...ou, pour les constantes comprises entre $00000080 et $00004000 :

Assembleur

Toutes ces petites astuces ne permettent d'économiser, finalement, que 2 octets et/ou 4 cycles en moyenne. Mais en additionnant à chaque fois la place et le temps gagné dans un programme de plusieurs ko, ce gain apparaît vite beaucoup plus appréciable.

Toutes les optimisations vues jusqu'ici ne s'appliquaient qu'aux registres de données, mais il existe des règles supplémentaires s'appliquant aux registres d'adresse.

Utiliser la forme ".W" chaque fois que possible. Par exemple, pour charger une adresse constante dans a0, vous écririez peut-être :

Assembleur

...alors que l'instruction LEA s'avère bien meilleure :

Assembleur

Bien sûr, pour mettre à zéro un registre d'adresse, rien ne vaut l'instruction SUB :

Assembleur

La forme ".W" de l'instruction PEA est très pratique pour empiler des valeurs dans le cas d'appel d'une fonction C. Au lieu d'utiliser :

Assembleur

...ou même :

Assembleur

...il vaut mieux écrire :

Assembleur

Ceci ne présente pas d'avantage particulier par rapport à la seconde méthode, du moins pour les petites constantes que MOVEQ accepte (si ce n'est d'éviter la frappe d'une ligne de code source ! Amis fainéants, bonjour). Par contre, pour les constantes 16 bits, on y gagne beaucoup. Sur un 68020 ou un 68030, l'instruction MOVEQ est encore plus rapide, grâce au système de "pipeline" utilisé dans ces processeurs. Vous avez donc à utiliser la seconde méthode chaque fois que possible.

Sur Amiga, un problème courant avec les registres d'adresse, est la conversion de pointeurs BPTR en pointeurs APTR (NDLR : un pointeur de type BPTR vous vient tout droit du langage BCPL, avec lequel a été écrit une petite partie du système d'exploitation de l'Amiga, notamment quelques bibliothèques. Un pointeur de type APTR est quant à lui en provenance direct du langage C). Un pointeur BPTR se distingue par le fait qu'il est décalé de deux bits vers la droite, opération impossible sur un registre d'adresse. La première solution venant à l'esprit est donc :

Assembleur

Même en utilisant une instruction MOVE.L au lieu d'EXG (ce qui soit dit en passant perd le contenu de d0), on ne gagne que quatre cycles. C'est pourquoi il vaut mieux écrire :

Assembleur

Un décalage de deux bits vers la droite revient en effet à multiplier par quatre.

Il existe certainement d'autres optimisations possibles en assembleur 68000, mais ce sont là les plus courantes. La meilleure manière de les retenir, et même d'en apprendre d'autres, est encore d'avoir un livre officiel de chez Motorola indiquant pour chaque instruction le temps d'exécution.

Quoiqu'il en soit, tout ceci ne représente qu'une petite partie de ce qu'il est possible de faire pour optimiser un programme entier. Il existe d'autre moyens que de bêtes substitutions d'instructions.

L'optimisation locale

Contrairement à tout ce que nous avons vu jusqu'ici, l'optimisation locale requiert un peu plus de réflexion quant aux moyens à utiliser. L'exemple le plus simple qui vienne à l'esprit est la multiplication. Ainsi, pour adresser un élément particulier d'un tableau, il est nécessaire de multiplier son indice par la taille maximale des éléments. En assembleur, un bon programmeur se débrouille toujours pour jongler avec les puissances de deux. Au lieu de :

Assembleur

Il écrira :

Assembleur

En ce qui concerne les décalages de bits, plusieurs règles sont à observer, qui sont résumées dans le tableau ci-dessous :

Assembleur
Assembleur

Toutes ces séquences sont plus rapides que :

Assembleur

De plus, elles évitent l'utilisation d'un second registre. Elles sont particulièrement efficaces pour des décalages entre 9 et 15 bits. Pour des décalages de 16 bits et plus, rappelez-vous de toujours effectuer le SWAP au bon moment, suivant le sens du décalage.

Il est important de noter que tous ces décalages "maison" ne s'occupent absolument pas d'un éventuel bit de signe, mais il est vrai qu'en assembleur, on ne s'en soucie que rarement. Si vous avez vraiment besoin du bit se signe, utilisez l'instruction EXT.L en lieu et place de MOVEQ=0.

Une optimisation particulière concerne l'écriture d'un logiciel assembleur... en assembleur : lorsque l'on génère le code machine d'un registre quelconque, il faut d'abord masquer son numéro avec la constante 7, puis décaler ce résultat de 9 bits vers la gauche. Un bon programmeur aurait écrit quelque chose du style (en considérant que le numéro du registre se trouve dans d0) :

Assembleur

Mais un programmeur (un peu plus) astucieux aurait certainement écrit :

Assembleur

La première instruction décale tout d'abord l'octet de poids faible du registre d0 de 5 bits vers la gauche, ce qui "élimine" au passage les bits supérieurs et donc réalise le AND=7 escompté. La seconde instruction effectue enfin un décalage de 4 bits vers la gauche, complétant ainsi le décalage de 9 bits souhaité.

Ne jamais oublier que certaines instructions affectent les bits du CCR

Un bon programme en assembleur ne comporte pour ainsi dire aucune instruction TST. Le 68000 est en effet suffisamment bien conçu pour positionner les bits du CCR en fonction des opérations effectuées, tout seul comme un grand. Le seul cas ou le CCR n'est pas affecté est lors de l'initialisation d'un registre d'adresse.

Mais chaque médaille à son revers : si vous modifiez après coup un programme pourtant considéré comme terminé, rien ne dit que vous n'allez pas modifier du même coup le CCR. C'est pourquoi il est parfois plus sûr d'utiliser l'instruction MOVEM, plus gourmande en mémoire et en temps d'exécution que MOVE, pour sauvegarder des registres, etc.

Je ne vous apprendrais rien en vous disant que tester les bits du CCR permet de réagir à certaines conditions, pour programmer des branchements divers. L'assembleur offre le luxe de pouvoir tester trois conditions en une seule fois :

Assembleur

C'est une situation que l'on ne retrouve dans aucun langage évolué (exception faite du Fortran). Lorsqu'on écrit de tels branchements, il est important d'avoir bien en tête celui qui aura le plus de chance de se produire. En d'autres termes, il faut être sûr que la condition que l'on va tester est celle qui a le moins de chances d'être vérifiée. Ainsi, vous éviterez à chaque fois la perte de temps occasionnée par le saut lui-même :

Assembleur

Abusez de l'instruction DBcc

La meilleure instruction du 68000 est encore DBcc (décrémentation d'un registre de données et branchement conditionnel). Même sur un 68010, toutes les instructions comprises entre DBcc et l'adresse du branchement sont copiées dans une mémoire cache interne, ce qui accélère leur traitement. Il faut toutefois porter une attention toute particulière à l'instruction DBRA :
  • Le compteur de boucle est sur 16 bits, les 16 autres restant inchangés.
  • Il serait idéal de n'inclure qu'une seule instruction dans la boucle.
  • La boucle est effectuée une fois de plus que spécifiée dans le compteur.
L'utilisation de DBRA la plus courante, est sans nul doute la copie de portions de mémoire :

Assembleur

Mais les instructions DBcc sont trop puissantes pour les limiter aux copies de mémoire. Elles sont particulièrement adaptées aux comparaisons, à l'exploration et au remplissage de zones de mémoire :

Assembleur

L'optimisation globale

Nous avons déjà fait un grand pas en avant, en économisant encore quelques octets et quelques cycles par-ci par-là. Il nous reste encore à optimiser le programme dans son ensemble.

Choisissez vos registres avec beaucoup d'attention

Un bon programme essaie de charger un ou plusieurs registres avec une valeur ou une adresse souvent utilisée, et n'y touche plus par la suite. Cette valeur est ainsi accessible en permanence.

Le meilleur exemple sur l'Amiga est AbsExecBase, qui devrait être en permanence accessible dans le registre a6, quitte à le sauvegarder ou à le réinitialiser lors d'appels à d'autres bibliothèques qu'Exec.

Évitez MOVEM autant que possible

Il est souvent utile, lors d'appels à un sous-programme, de sauvegarder les registres corrompus pour les récupérer en revenant. Essayez plutôt de faire en sorte que vos routines n'utilisent pas les mêmes registres que le programme principal.

Évitez LINK et UNLK autant que possible

LINK est souvent utilisée dans les langages compilés, tel le C. On ne la croise par contre que rarement dans un programme en langage machine (NDLR : elle sert à créer un espace mémoire utilisable pour sauver des données trop grandes pour un registre. En pratique, on l'utilise surtout lors de la programmation de routines récursives). Quoiqu'il en soit, il est de loin préférable de créer une sous-routine supplémentaire chargée de sauver les valeurs, voire carrément de les empiler.

Choisissez vos drapeaux avec attention

Si un octet de mémoire vous sert de drapeau à trois états, il est plus efficient de lui affecter comme valeurs possibles -1, 0 et 1, ce qui permet des branchements plus rapides (avec Bcc, sans TST ni CMP avant).

Utiliser les tableaux comme il faut !

Beaucoup d'opérations peuvent être réalisées à l'aide de tableaux. Par exemple, pour une rotation de bloc dans un programme de dessin, il est préférable de calculer auparavant les 360 valeurs du sinus et de le stocker dans un tableau, plutôt que de les calculer par programmation.

L'artillerie lourde

Tout ceci ne représente en aucun cas une liste exhaustive et immuable de ce qu'il faut faire pour améliorer ses programmes en assembleur 68000. Avec un peu de pratique, tout ce qui a été dit ici deviendra partie intégrante de vous-même, comme un réflexe de programmation.

En moyenne, seulement 5% d'un programme monopolisant quelques 95% de son temps d'exécution. Il y a beaucoup à gagner en s'occupant d'optimiser les 95% restants...

Dernier truc

Apprenez à utiliser le bit X

Le bit X du CCR est une des choses les plus étranges du 68000. Il n'est influencé que par peu d'instructions, et n'est de fait presque jamais utilisé. Il possède pourtant la très intéressante particularité d'initialiser un registre à 0 ou à -1, d'après sa valeur initiale : subx.1 d0,d0.

Voilà. Nous allons terminer avec un petit test, histoire de voir si vous feriez un bon "optimiseur" : une valeur quelconque est dans d0, et l'on ne connaît pas l'état du CCr. Initialisez d1 à 0 si d0 est nul, et à 1 dans le cas contraire. Vous pouvez au besoin perdre le contenu de d0.

Solution :
neg.L   d0
subx.L  d1,d1
neg.L   d1



[Retour en haut] / [Retour aux articles]