|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Voici un article visant à vous expliquer les particularités du langage machine des processeurs de la famille PowerPC, vu depuis le monde des Amiga. On ne va pas rentrer dans les détails, mais on relèvera ses particularités. Programmer cet assembleur est extrêmement intéressant, car il permet de se faire une image exacte du code qui va être exécuté, et on peut en théorie se permettre certaines bidouilles. Mais si du temps du 68000, l'assembleur était nécessaire pour obtenir les meilleures performances, ce n'est plus le cas avec le PowerPC, pour plusieurs raisons :
1. Quelques spécificités du matériel Le PowerPC a été inventé par Motorola, IBM et Apple au début des années 1990, et Motorola/IBM ont fabriqué leurs versions de chaque génération de PowerPC. Il s'agissait au départ de faire un processeur RISC grand public. La technologie RISC (Reduced Instruction Set) s'oppose au plus classique CISC (complex Instruction Set). L'idée est d'avoir moins d'instructions, ce qui permet d'accélérer le traitement. Une autre génération de PowerPC professionnel est dénommé "POWER" (Pipeline Orientation With Enhanced RISC) en opposition aux "PowerPC" actuels que nous utilisons. Ces deux architectures ont divergé (d'aucuns disent que le PowerPC n'est plus RISC) et utilisent un jeu d'instruction sensiblement différent : dans les catalogues d'instructions que vous trouverez, certaines ne sont valables que pour les "POWER". En interne, les PowerPC peuvent ou non gérer des entiers 64 bits mais toujours des flottants 64 bits. L'ABI utilisée ensuite par le système devra en prendre compte. Pour nos Amiga, les registres entiers sont en 32 bits (jusqu'à nouvel ordre) mais les flottants sont utilisables en 32 (float) ou 64 bits (float double). Il existe des instructions pour les entiers 64 bits mais l'ABI décide de leur disponibilité. Encore plus fort : l'ABI peut aussi décider de l'ordre gros-boutiste/petit-boutiste ! Nos Amiga, comme les 68000 et les Mac, sont en gros-boutiste. Il est possible d'imaginer un système PowerPC complet implémenté nativement en petit-boutiste ! 2. Le plein de registre Ce qui frappe le néophyte sur l'assembleur PowerPC, c'est le nombre de registres internes :
3. Un processeur qui mise sur ses caches Les instructions PowerPC assemblées font en général quatre octets, alors que sur les 68000 et les processeurs Intel, on a plutôt deux octets. Cela signifie qu'une fois assemblé, un code PowerPC peut être plus long. Nous verrons que pour d'autres raisons, il est parfois plus court. Mais ce n'est pas fini : 4*32 + 8*32 = 384. Les registres du PowerPC pèsent 384 octets. Chaque fois qu'on veut stocker un état des registres, c'est 384 octets qui doivent être écrit puis lu. Si une tâche utilise le VPU (Vector Processing Unit, l'AltiVec), on atteint un kilo-octet de registres internes, quand un 68000 n'avait que 64 octets ! Cela reflète la politique PowerPC de se baser sur ses gros caches internes : les problèmes de performances du bus liés à tous ces accès sont annulés par l'efficacité du cache. Par exemple, la pile d'une tâche ne va réaliser vraiment ses écritures/lectures qu'en dernier recours. 4. La syntaxe, le système de mnémonique Dans les faits, le PowerPC est un des processeurs qui possède le plus d'instructions assembleurs bien qu'il soit RISC. Il existe une syntaxe de base définie par IBM : "#" pour les commentaires, les mnémoniques des instructions sont clairement définie, comme les "pseudo op" : les directives de compilations, (commençant par "." comme ".align") qui sont définies aussi. Ensuite, les applications d'assemblage (passembleur, Powerassembleur sur Amiga) peuvent rajouter d'autres mnémoniques et d'autres pseudo op en interne ! Nous allons vous apprendre à lire l'assembleur PowerPC. Si vous lâchez un assembleuriste 68000 ou x86 sur de l'assembleur PowerPC, il ne va rien comprendre. En 68000, vous aviez une instruction "move" unique pour réaliser une copie de valeur, entre registres ou mémoires. Le format de données était explicite, donné par ".b" (un octet) ".w" (deux octets) et ".l" (quatre octets). La source et la destination du "move" pouvait être un registre ou une mémoire, et il y avait plusieurs modes d'indexations possibles. En PowerPC, vous trouverez un jeu d'instructions pour remplacer "move". Notez que pour toutes les mnémoniques assembleur PowerPC, le registre affecté ou lu est toujours à gauche :
Attention : les mnémoniques PowerPC sont déjà des abstractions par rapport au code assemblé ! Il existe un jeu d'instructions PowerPC officiel ("lwz" et "mr" en font partie) mais parmi elles certaines sont déjà des macros : "mr r4,r5" est une macro équivalente à "or r4,r5,r5". Ne cherchez pas d'équivalent au "tst.l" du 68000, rajoutez un "." à l'instruction précédente, ou au pire faite "or. r5,r5,r5". De même, beaucoup d'instructions de décalage officielles sont des macros de "rlwinm". Pour l'assembleur passembleur, certaines de ces macros sont déclarées en interne, d'autres font appel à un fichier d'en-tête spécial. N'importe qui peut créer ses propres macros, mais je ne le conseille pas. Certaines macros ont l'air intéressantes, mais elles correspondent parfois à plusieurs instructions, donc on perd la lisibilité des cycles effectivement exécutés pour un code. 6. Un gros piège : la notation des registres Comme je l'ai déjà indiqué, les registres entiers sont notés de r0 à r31, il faut écrire "mr r5,r6" (copie r6 sur r5). Mais voilà, ces "rx" sont des macros ! L'assembleur PowerPC peut se contenter de cet équivalent : "mr 5,6" ou même "or 5,6,6". C'est également vrai pour les registres flottants notés de f0 à f31. Je vous conseille fortement d'utiliser uniquement les notations "rx" et "fx" à chaque fois qu'il s'agit de registres, et de n'utiliser des valeurs entières que pour décrire des valeurs entières, des valeurs de décalages ou de positions de bits, sinon votre code sera illisible. 7. Le FPU : l'unité gérant les nombres réels flottants Comme le FPU n'est jamais en option, je vous encourage à les utiliser autant que possible, ce qui laisse d'autant plus de registres libres pour les entiers. Attention à cette particularité importante du PowerPC : tous les accès mémoire FPU, en float ou en double, en lecture ou en écriture, doivent se faire sur des adresses multiples de 4 ou une exception est générée ! Cela signifie que si on a cette structure C alignée sur quatre octets :
Si on réalise une lecture sur "mas_unfloat", il y a exception processeur, et sur la plupart des systèmes, cela se traduit par une émulation de l'instruction de lecture ou d'écriture (ralentissement significatif). Note : sur WarpOS, l'outil ShowHALInfo permet de tracer le nombre d'exceptions FPU émulées. Donc faites bien attention à aligner vos flottants. Autre particularité du FPU : il existe une instruction assembleur de conversion d'un flottant vers un entier, mais il n'existe pas d'instruction de conversion d'un entier vers un flottant, il faut taper l'algorithme "de normalisation" pour créer le type flottant IEEE. Cela peut être très frustrant. Le fait est que j'ai programmé un moteur 3D en assembleur PowerPC sans m'être posé la question ! En effet, dans la grosse majorité des algorithmes, on dispose des flottants en entrée, et la conversion vers les entiers se fait une fois pour toutes en cours de route (dans le cas d'un pipeline 3D, on dispose des flottants décrivant la géométrie en entrée, et le passage en entier se fait seulement au niveau de l'écriture des pixels). Les mêmes registres flottants peuvent être utilisés en tant que "float" quatre octets ou "float double" huit octets. Les fonctions de lecture/écriture mémoire (lfs, stfs ou lfd,stfd) décident de leurs types. Certains "cast" entre float et double écrits en c/C++ sont donc implicites une fois compilés. A noter que toutes les instructions de calcul flottant existent en version double et simple (on ajoute "s" dans ce cas) sauf pour la copie de registre à registre valable pour les deux : "fmr". Quelques mnémoniques flottantes intéressantes :
Le calcul ci-dessus peut être compilé en une instruction PowerPC ! (code plus court). Ce n'est pas tout :
Car il n'existe pas d'affectation immédiate flottante en PowerPC. Ce serait un moindre mal, mais les compilateurs actuels vont ajouter des données globales pour chaque écriture d'une valeur dans vos sources C, même si cette valeur apparaît plusieurs fois. Pour ces raisons, écrivez toujours "0.2f" et pas "0.2" quand vous le pourrez. La solution ultime en C est de créer son ".o" de valeurs flottantes globales récurrentes, et affectez ces variables globales à des variables locales le moment venu, au lieu d'écrire des valeurs directes dans vos équations. Cela accélérera vos calculs FPU (chez Intel, c'est pareil). Avis aux petits malins qui pensent optimiser en pensant que ceci donne la valeur 0.0f à f0 :
Cela ne marchera pas si f0 est "NaN" (Not a Number). On fait les malins et voilà ce que ça donne. 8. La plupart des instructions pour le même prix ! La grande majorité des instructions PowerPC prennent un cycle. C'est le cas des additions, soustractions, opérations logiques, mais aussi des multiplications et décalages. Beaucoup d'optimisations autrefois valables pour la famille 680x0 ne se justifient plus :
...est aussi rapide que :
Du côté du FPU, les additions et multiplications prennent également un cycle. La grosse exception reste les divisions. Les cycles varient en fonction de la génération de processeur :
...mais cela (la valeur 0.2f sera stockée et aucune division réalisée) :
Mais c'est peut-être automatique pour votre compilateur. 9. Le PowerPC peut paralléliser beaucoup de calculs Le PowerPC peut également paralléliser un grand nombre de calculs dans les mêmes cycles si des instructions consécutives utilisent des registres différents et des instructions permettant cette parallélisation (notion de pipeline dans le processeur, qui parallélise les instructions). Le FPU peut aussi travailler en parallèle avec les entiers, dans les mêmes cycles. Par exemple :
10. Les mnémoniques de décalage de bits Le PowerPC est très puissant en décalage de bit. A noter qu'en PowerPC, les bits sont numérotés dans l'ordre inverse de la convention 68000: le bit de poids le plus fort est le zéro, le bit de poids le plus faible est noté 31 (les sens gauche-droite de nature schématique, restent les mêmes). D'autre part, il semble que les bits notés 0 à 31 en 32 bits deviennent les bits 32 à 63 dans une ABI PowerPC 64 bits !). L'instruction "Rotate Left Word Immediate Then AND with Mask" demande cinq paramètres !
Pour le même prix, on peut aussi utiliser "rlwimi" (Rotate Left Word Immediate Then Mask Insert) qui prend les mêmes paramètres, mais insère la suite de bits dans le registre destination, laissant les autres bits inchangés. Le compilateur sait utiliser au mieux ces instructions :
...peut être compilée en une instruction assembleur PowerPC, qui plus est parallélisable. Là encore, on voit qu'un code PowerPC est parfois plus court. Les décalages algébriques (signés) à droite comme sraw et srawi ne sont pas des mnémoniques de ces instructions. 11. les mnémoniques logiques et arithmétiques Les mnémoniques logiques et arithmétiques ont trois paramètres :
12. Des instructions de manipulation du cache puissantes Ces instructions ne sont jamais et ne peuvent pas être utilisées par le compilateur, pourtant leurs maîtrises permettent d'accélérer de façon significative certaines routines ! Mais attention, il faut comprendre le mécanisme du cache et surtout le fait que les lignes de cache sont lues et écrites par bloc de 32 octets, lesquelles sont alignés sur des adresses multiples de 32. Si on fait un accès en lecture ou écriture, la ligne de cache "touchée" est d'abord chargée dans le cache, et l'opération effectuée dans le cache. Le cache sera validé en écriture RAM plus tard, à un moment indéterminé.
"Data Cache Block Touch" permet de préparer le chargement d'une ligne de cache, une zone mémoire de 32 octets qui sera ensuite lue plus vite par les instructions suivantes. Cela permet de paralléliser le chargement des données par le bus. L'adresse affectée est donnée ici par r4+r5.
"Data Cache Block to Zero". Encore plus fort, le fin du fin : lorsque vous effacez une zone mémoire, habituellement, vous mettez des registres à zero puis vous écrivez ces registres. Chaque ligne de cache va donc charger une mémoire en cache où elle va se faire écraser aussitôt par des zeros. "dcbz" permet de déclarer les 32 octets d'une ligne de cache comme chargés et mis à 0 sans chargement par le bus, ce qui accélère considérablement cette écriture. Dans les faits, et après des tests de performances sérieux, toutes écritures visant à écraser 32 octets à la suite peut être accélérée par un facteur 4 avec "dcbz". L'adresse affectée est donnée ici par r4+r5. Il existe tout un jeu d'instructions de manipulation du cache interne ("dcbst" qui force une écriture dans l'instant, "dcbi" qui invalide une ligne de cache, etc.). Notez que chacune des instructions "dcb(x)" peut potentiellement fonctionner ou pas, selon le contexte : certaines zones mémoire (la mémoire vidéo par exemple) ne peuvent pas être mises en cache. Un "dcbz" peut donc échouer selon la zone concernée. Précision importante : les instructions "dcb(x)" sont faites pour manipuler des lignes de cache de 32 octets, avec lesquelles les PowerPC travaillent depuis les premières générations jusqu'au G4. Sur le G5, les lignes de cache sont à 128 octets. D'après des documentations d'Apple, sur G5, "dcbz" charge implicitement 128 octets et écrit 32 d'entre eux à zéro. Il a donc le même comportement qu'une écriture classique et ne génère pas de bogue. De plus, une nouvelle instruction G5, "dcbzl" permet d'effacer les 128 octets d'un CL sans lecture implicite. Selon une autre documentation IBM, le "dcbz" prend l'un ou l'autre comportement selon le système. Il est donc conseillé d'utiliser "dcbz" seulement après avoir testé que votre processeur est un G4 ou inférieur. 13. Les sauts gratuits et les huit registres d'état L'instruction "b label" permet de faire un saut simple jusqu'à un label. Si le code exécuté et le code de destination du saut se trouvent déjà dans le cache instruction, le saut lui-même prend zéro cycle. Il est instantané. Encore plus fort : un test, quel que soit l'assembleur, se déroule en deux temps. On met à jour un registre d'état, par exemple en comparant deux nombres, puis on réalise un branchement (saut) ou non, vers un code particulier d'après cet état (logique du "if" en C et BASIC). En assembleur PowerPC, si la mise à jour du registre d'état à été faite trois cycles avant le branchement de test, et si tous les codes concernés sont en cache instruction, ce branchement prend également zéro cycle. Il est instantané. Si vous avez compris, en PowerPC, un test peut potentiellement prendre zéro cycle ! Dans les 680X0, on avait un registre d'état qui stockait les dépassements, l'égalité à 0, le "plus petit que" et le "plus grand que", ce qui permet ensuite de faire des branchements (saut de test). Sur PowerPC, on a huit registres d'état notés cr0, cr1, cr2, ..., cr7 (en fait tous font partie d'un registre entier spécial accessible) qui peuvent stocker huit images de ces états. L'option de test "." affecte toujours "cr0" :
Des instructions de comparaison comme "cmpwi" peuvent affecter l'un ou l'autre :
Les instructions de branchement conditionnel peuvent utiliser l'un ou l'autre :
...ou pas : utilisation implicite de cr0 :
Il est possible de faire des copies d'état entre ces registres :
Cependant, le code généré par les compilateurs n'utilise que cr0, cette capacité à retenir plusieurs registres d'état n'est accessible que depuis l'assembleur (même chose pour la famille des processeurs Intel).
|