Nous présentons dans ce chapitre et le suivant les améliorations apportées au processeur de base : FPU, MMX, 3DNow! et SSE. Il est clair qu'en 2004, la FPU et même les jeux MMX et SSE font partie du standard de fait, avant l'arrivée d'AMD64 qui les officialisera certainement. Ce regroupement est justifié par le fait que la technique est à chaque fois la même : ajout de quelques registres et d'un certain nombre d'instructions et d'exceptions. Certaines de ces technologies ont de plus en commun d'être mutuellement exclusives à un instant donné, puisqu'elles partagent les mêmes registres. C'est en tout cas de cette façon qu'elles nous apparaissent. Voyons rapidement le contexte historique de leurs apparitions.
Coprocesseur arithmétique, FPU ( Floating-Point Unit ), unité à virgule flottante, copross, tous désignent le même objet, une unité destinée à faciliter et à accélérer le calcul de haut niveau. Évacuons le terme coprocesseur, qui vient du fait que, du 8086/8088 au 80386, il s'agissait d'un circuit optionnel, simple à installer sur l'IBM PC puisqu'il suffisait d'insérer une puce à 40 pattes dans un support prévu à cet effet. De référence 8087, 80287 et enfin 80387, ce circuit était couplé à celui de l'unité centrale de façon étrange, par une séquence initiée par le préfixe du code machine de l'instruction. Ce couplage était transparent pour le programmeur même en assembleur, qui devait en revanche se préoccuper de la disponibilité ou non de FPU sur la machine cible.
Les compilateurs, Fortran, C, Pascal proposaient l'option de générer du code avec et sans coprocesseur. Pour l'assembleur, il existe des bibliothèques d'émulation. Leur usage peut être rendu transparent par le traitement de l'exception 7 #NM (No Math Coprocessor). La routine d'interruption émule l'instruction ou le bloc et rend la main au programme après cette instruction ou ce bloc. L'idée du bloc n'est qu'une proposition, nous pouvons également imaginer une émulation totale, instruction par instruction, avec maintien des flags et d'une copie des registres en mémoire. Il faut également émuler les exceptions générées par la FPU. En bref, du boulot.
Ce circuit était onéreux, et l'installer, particulièrement sur un clone à bas prix, n'était pas la règle. À partir du 80486, il est présent au sein du microprocesseur en tant qu'unité fonctionnelle, mais le nom est resté. Le 486SX était un 486DX dans lequel étaient désactivées les fonctionnalités de la FPU. Un nom correct serait unité de calcul mathématique. Nous utilisons souvent celui de FPU, au féminin.
Externe ou intégrée au microprocesseur, la FPU lui ajoute un bouquet d'instructions, quelques registres supplémentaires et un certain nombre d'exceptions. Aujourd'hui, elle est présente sur tous les modèles de microprocesseurs, même déjà anciens. La FPU actuelle est compatible avec le 8087, avec, depuis, beaucoup moins d'évolutions que le reste du microprocesseur. Quelques nouvelles instructions, mais toujours les mêmes formats de données traités. En revanche, les performances ont été considérablement améliorées, du fait en particulier des progrès de l'architecture. Il existe par exemple plusieurs FPU fonctionnant en parallèle dans certains processeurs. Elle fait maintenant partie intégrante de l'architecture.
Une série de coprocesseurs concurrents, et non de clones du 8087, la gamme Weitek, a eu son heure de gloire, avant l'intégration de la FPU directement dans les puces.
Quelques notions de mathématiques s’imposent avant de commencer l'étude de la FPU. Les initiés peuvent sauter cette partie, plutôt que d’en relever les approximations, volontaires ou non, dans le formalisme. Attention, contrairement à ce que pourrait laisser penser cette introduction, les calculs sur les nombres réels ne sont pas le seul intérêt de la FPU, loin s’en faut.
Les mathématiciens n'étaient soumis à aucune contrainte technologique quand ils ont créé les nombres réels. Ce sont des nombres aux propriétés parfois étranges. Disons pour simplifier qu'il s'agit des nombres à virgule, avec autant de chiffres que souhaité après celle-ci. Tous les nombres de notre vie courante sont des réels. Un entier, une fraction par exemple sont également des réels. Il existe des nombres réels qui ne sont rien d'autre, qui ne sont solution d'aucune équation algébrique disent les mathématiciens : ils sont nommés nombres transcendantaux. C'est le terme employé dans la documentation, à propos du fameux Pi (3,14) par exemple.
Les réels courent de l'infini négatif à l'infini positif, en passant par 0. Une de leurs propriétés peut s'exprimer par le fait qu'entre deux nombres réels, aussi proches que l'on veut l’un de l’autre, existe une infinité de nombres réels. Et ainsi de suite, si l'on peut dire. Un nombre réel n'a pas de nombre réel suivant. Ce sont les nombres du continu, par opposition au discret. Quant à savoir si ce sont ceux du réel, seul l'avenir le dira peut-être à nos enfants : pouvons-nous affirmer aujourd'hui que l'univers est fondamentalement continu ?
Quelques mots sur ce que représente, en langage de tous les jours, la notion de virgule flottante . C'est, en gros, la représentation scientifique de nos calculatrices et de nos cours de physique : -1,6.10 ‑2 ou 0,16 E-1, qui valent 0,016. 1,6 (ou 0,16) s'appelle la mantisse , positive dans cet exemple, et -2 (ou -1) l' exposant . Il existe, selon les domaines, plusieurs conventions légèrement différentes concernant la façon d'écrire la mantisse. Par exemple, la mantisse commence toujours par un (et un seul) chiffre non nul avant la virgule ou alors commence toujours par un 0, puis la virgule, puis un chiffre non nul.
Rappelons que 10 -n = 1/10 N . Donc l'exposant n positif consiste à multiplier la mantisse par 1 suivi de n zéros, ce qui reste valable pour n = 0, auquel cas la valeur du nombre est celle de la mantisse. Pour n négatif, la mantisse est à multiplier par 0,[n-1 zéros]1, par exemple 0,001 pour 10 -3 .
Cette méthode nous permet d'adapter au mieux notre capacité d'affichage ou de calcul à la précision et à l'ordre de grandeur de la donnée. Intuitivement, une précision de l'ordre du kilomètre est adaptée à la préparation d’un voyage en automobile, mais tout à fait insuffisante pour mesurer la surface d'une salle de séjour. Il s'agit de la notion de précision relative. Vous vous souvenez peut-être de la règle à calcul, magnifique objet qui évitait à son utilisateur quelques grossières erreurs, en le poussant à s'intéresser d'abord à l'ordre de grandeur du résultat, puis ensuite seulement à une raisonnable précision. Avec la même quantité d'information, nous pouvons, en virgule flottante, traiter la distance Terre-Soleil (1,496 E+11 m), la vitesse de la lumière (2,997925 E-8 m/s) et la charge de l'électron (1,6 E-19 C).
Pour bien se fixer les idées, imaginons une représentation flottante, sur la base de nombres décimaux, définie comme suit :
Une mantisse comprise entre 0 compris et 1 non compris. Quatre chiffres sont disponibles après la virgule (avant la virgule, c'est toujours 0).
Une puissance de 10, exprimée par un exposant entier, sur un seul chiffre signé, donc de -9 à +9 en passant par 0.
Un signe.
Cette représentation consomme 5 chiffres et deux signes. C'est-à-dire qu'elle pourra, au mieux, représenter 400 000 valeurs différentes, de 0 à 99 999, avec 4 combinaisons des deux signes. Oublions le signe de la mantisse pour terminer notre raisonnement.
Plus grand nombre représentable : +0,9999.10 9 , inférieur à 10 9 , ou 1 000 000 000. C'est le plus l'infini de cette représentation.
Plus petit nombre représentable, hors le 0 : 0,0001.10 -9 .
Maintenant, voici des données tout à fait intéressantes. Si nous utilisons brutalement nos 5 chiffres en notation entière, l'écart entre deux chiffres consécutifs serait constamment de 1. Dans notre notation flottante, il en va tout autrement :
Écart entre deux valeurs consécutives autour de 0, au-dessus en positif : 0,0001.10 -9 , soit 0,0000000000001. C'est bien. C'est mieux qu'avec la méthode normale.
Écart entre deux valeurs consécutives autour de l'infini, en positif ou négatif, peu importe : 0,0001.10 9 , soit 100 000. C'est moins bien. Beaucoup moins bien.
Nous voyons que la précision varie avec la taille du nombre (nous parlons de précision relative). Nous pressentons que, si nous sommes absolument limités à 5 chiffres et une seule représentation, les flottants permettront de décrire des phénomènes physiques plus différents. Compter des moutons sera en revanche moins agréable.
Nous voyons maintenant assez bien ce que sont des réels, sur la base d'une représentation décimale. Il faut donc voir comment l'ordinateur va adapter cela à sa numération essentiellement binaire.
Beaucoup de problèmes d'ingénierie impliquant les réels, il devint à un moment de l’histoire de l’informatique souhaitable de mettre en place des méthodes ou des composants en vue de faciliter leur traitement par l'ordinateur. Le calcul scientifique représentait dans les années soixante-dix une préoccupation importante en pourcentages des utilisateurs d'ordinateurs. Ce n'est par hasard que Fortran (Formula Translator) faisait partie des tout premiers langages de haut niveau.
En premier lieu, il fallut représenter les réels en machine. L'IEEE (Institute of Electrical and Electronics Engineers) a normalisé, par les normes IEEE 754 et IEEE 854, non seulement la représentation mémoire de ces nombres, mais également leur comportement arithmétique. Cette norme est d'ailleurs directement issue des travaux menés chez Intel autour de la FPU.
L'utilisation du mot flottant pour désigner les réels permettra de conserver à l'esprit le fossé qui existe entre de vrais réels et leur représentation en machine. C'est une erreur de programmation courante que d'utiliser des flottants là où des entiers conviendraient. Les plans graphiques sont des tableaux d'entiers et traiter un cercle à l'aide de fonctions trigonométriques (sinus et cosinus) sur des réels n'est pas toujours une bonne idée.
Un des rôles, mais pas le seul, de la FPU sera de traiter les nombres réels, en accord avec les normes IEEE. Intel présentait d'ailleurs la FPU comme une façon de permettre à l'ingénieur d'utiliser l'ordinateur en conservant ses habitudes de calcul papier-crayon, en minimisant les efforts de programmation et en évitant autant que possible les erreurs classiques d'arrondis.
Un développement mathématique sur ce type d’erreurs dépasserait le cadre de cet ouvrage. Essayons néanmoins de susciter notre intuition par l'exemple suivant. Deux objets A et B sont à une distance très grande DA et DB d'un troisième S : prenons pour l'exemple deux objets A et B à la surface de la terre, à une distance de l'ordre de 36 000 km d'un satellite S. Posons que ce qui nous intéresse, c'est la différence DA - DS, de l'ordre du mètre, avec une précision de l'ordre du dixième de millimètre. Si nous procédons à la cosaque, mesure de DA et DB puis différence, il nous faudra mesurer 36 millions de mètres avec une précision du 1/10 mm, soit une précision relative de 3.10 -12 . Si en revanche nous trouvions une méthode pour mesurer directement la différence DA - DS, la précision requise ne serait plus que de 10 -4 , plus réaliste. Dans une simple formule de calcul, l'ordre dans lequel ces calculs seront réalisés pourra ainsi rendre le résultat aberrant ou correct.
Nous allons maintenant aborder la représentation d’un nombre réel à l’aide d’un nombre déterminé de bits. Soyons clair, le sujet est ardu. Néanmoins, sachez que sa maîtrise n’est en rien nécessaire pour programmer la FPU en assembleur et pour en retirer les avantages. La plupart des points les plus rébarbatifs sont en rapport avec des comportements marginaux de l’unité de calcul, face à des cas particuliers. Muni d’un résumé du jeu d’instructions, il sera possible de programmer efficacement le coprocesseur, en le voyant comme une calculatrice scientifique.
Le sujet étant comme il vient d’être noté suffisamment complexe, nous ne ferons généralement pas la différence entre ce qui est propre à l’architecture IA et ce qui découle des normes IEEE. De plus, nous raisonnons sur un seul format dans un premier temps.
Nous disposons de 32 bits pour coder un réel. Nous allons utiliser une structure signe + mantisse + exposant.
Nous sommes en binaire, l’exposant sera appliqué à 2 et non plus à 10. Pour les exposants négatifs, les 1/10 (0,1), 1/100 (0,01), 1/1000 (0,001) du décimal deviennent de ½ (0,5), ¼ (0,25), 1/8 (0,125), etc.
1 bit est réservé au signe (0 pour positif, 1 pour négatif). Nous réservons ensuite les 8 bits suivants à l’exposant, les 23 qui restent exprimant la mantisse. Ce qui se représente par :
SEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMM
En première approche, ce nombre sera : S * (-1) * 1.MM..MMM * 2 EE..EE (à partir de maintenant, nous utilisons le point décimal en lieu et place de la virgule).
S*(-1) représente simplement le signe.
La mantisse commençant toujours par le chiffre 1 suivi du point décimal, il est inutile de stocker ce renseignement qui n’est pas une information.
Nous n’avons pas encore envisagé le codage des exposants négatifs, alors que cela est absolument nécessaire. Il aurait pu être décidé de le coder en complément à deux. Pour diverses raisons, ce n’est pas la solution qui a été retenue. La valeur de l’exposant est décalée d’une, pour être à peu près centrée sur 0. Dans notre cas, la constante de décalage est 127. Décalé traduit l’anglais biased, qui pourrait littérairement se traduire par biaisé, voire tendancieux.
Il est important, à ce niveau, de voir ce qui se passe, avant de continuer sur des notions plus élaborées. Effectuer les conversions à la main est relativement complexe. Nous avons donc repris notre fonction IntToBin du chapitre intitulé Assembleurs intégrés et bâti un petit projet :
var
fp0 : Single;
u32 : Longword;
begin
fp0 := StrToFloat(EdSaisie1.Text);
asm
mov eax, dword ptr fp0
mov u32, eax
end; //asm
MemoSortie.Lines.Add(FloatToStr(fp0));
MemoSortie.Lines.Add(IntToBin(u32, 8));
end;
Notez encore une fois la facilité offerte par l’assembleur intégré pour effectuer un transfert entre deux variables, sans toute une tuyauterie de pointeurs.
Nous obtenons les résultats.
Les bits ont été manuellement réarrangés en paquets de 1, 8 et 23.
Le comportement du bit de signe est conforme à nos attentes.
Les 8 bits de l’exposant valent 128 + 2 = 130.
La mantisse s’écrit 1.10001, en binaire à virgule.
En utilisant un tableur, en calculant les coefficients des chiffres après la virgule par multiplications successives par 2, nous trouvons la valeur de ce 1.10001 :
1
1 1
1
0,5 0,5
0
0,25 0
0
0,125 0
0
0,0625 0
1
0,03125 0,03125
1,53125
Tant que nous tablons, calculons la valeur par laquelle multiplier ce 1.53125 pour obtenir le résultat :
1,53125 1 1,53125
1,53125 2 3,0625
1,53125 3 4,59375
1,53125 4 6,125
1,53125 5 7,65625
1,53125 6 9,1875
1,53125 7 10,71875
1,53125 8 12,25
1,53125 9 13,78125
1,53125 10 15,3125
1,53125 11 16,84375
Les 8 bits de l’exposant valaient 130. Débiasés de 127, ces 130 en valent 3. Or, 2 3 = 8. Bingo.
Les trois formats de réels de la FPU fonctionnent sur le même principe, avec une nuance pour l’extended 80 bits. Ces formats (nombre de bits affectés à l’exposant et à la mantisse) sont donnés plus loin dans ce chapitre, y compris de façon graphique.
Il est important que ce qui vient d’être vu soit clair. Il est préférable d’y passer un moment (tester les flottants de 64 et 80 bits par exemple), quitte à sauter la fin de la partie, plutôt que de continuer sur des bases floues.
Nous pouvons remarquer que nous ne savons pas représenter le 0 de cette façon : 2 à n’importe quelle puissance ne peut pas être nul, et 1.xxxxx, encore moins. Si nous introduisons 0 dans notre moulinette, le flottant résultant a tous ses bits à 0. Ce ne peut être que conventionnel, puisque :
1.0 * 2
-127
= quelque_chose_de_tout_petit_mais_quelque_chose_quand_même
Nous avons modifié une ligne de notre programme (traitement du bouton 2) :
fp0 := StrToFloat(EdSaisie1.Text);
u32 := StrToInt (EdSaisie1.Text);
fp0 := fp0 - u32;
Le résultat doit être proche de, ou égal à, 0. Nous avons testé le code avec 12, puis -12, pour les résultats suivants :
0
0000 0000 0000 0000 0000 0000 0000 0000
-4294967296
1100 1111 1000 0000 0000 0000 0000 0000
Si le premier résultat n’a rien de vraiment étonnant, le second est assez étrange. Vérification faite à l’aide du débogueur, c’est bien dans la FPU que ça se passe. Le résultat est le même avec -1, -1000, etc. Le nombre est négatif, la mantisse nulle (ou égale à 1.000). De plus, 4294967296 n’est pas quelconque : c’est 100000000h , c’est-à-dire le plus grand des nombres 32 bits plus 1.
Récapitulons. Nous disposons d'un moyen correct de bien représenter les réels, sur la base des règles suivantes :
Le signe a son propre bit, indépendant des autres éléments. Cela veut dire que chaque nombre positif a son image négative (y compris le 0).
L'exposant est un nombre positif, décalé (biased) vers les négatifs d'environ la moitié de sa valeur maximale, ce qui permet de répartir l'efficacité du codage vers le très grand comme vers le très petit.
La mantisse est composée d'un 1 implicite (non stocké dans le nombre, non variable, un peu comme les 0 qui suivent le score sur un flipper). Les bits dont nous disposons sont placés à la suite de ce 1 et d'une virgule, pour obtenir une valeur normalisée de mantisse comprise entre 1 et 2 (non compris).
Seul le 0 nous pose un problème. Et puis, nous aimerions bien conserver la possibilité de quelques valeurs particulières pour coder des situations atypiques. Si nous savions coder les infinis positifs et négatifs, nous pourrions traiter de façon propre certains dépassements de capacité. Et nous savons que si nous divisons un nombre quelconque mais non infini par l'infini, nous obtenons 0. Nous savions même au lycée obtenir dans ces cas-là un 0+ et un 0-.
Notre représentation étant copieuse (nous allons ensuite passer à 64, puis 80 bits), nous sacrifions deux valeurs d'exposant. Il est bien clair que nous ne pouvons pas nous priver d'une valeur au milieu de la gamme (ça laisserait un trou). Nous déclarons donc hors norme les deux valeurs extrêmes de l'exposant : 0 et 255.
Reste quand même 1 à 254, donc, une fois décalés, de -126 à +127, ce qui reste sera suffisant. Servons-nous, en parant au plus urgent :
0 00000000 00000000000000000000000 : 0 (le vrai) ;
1 00000000 00000000000000000000000 : -0 ;
0 11111111 00000000000000000000000 : + l'infini ;
1 11111111 00000000000000000000000 : - l'infini.
Nous aimerions également que le processeur puisse parfois faire preuve de modestie. Nous en connaissons tous des processeurs qui, au sortir d'une violente exception, vous donnent quand même un beau résultat bien frais. Deux minutes avant, il était bloqué pour cause de division par 0, mais là, il vous donne un résultat. Nous avons donc sorti de notre stock :
1 11111111 10000000000000000000000 : on ne sait pas, indéfini.
Il nous reste des nombres non utilisés. Nous aimons classer, nous constatons qu'il y en a de deux types :
Les gros, avec un exposant à 255. Ceux-là sont antipathiques. Sans savoir pourquoi, c'est physique. Plus grand que le plus grand des nôtres, que nos infinis même. Ils n'auront pas droit à l'appellation de nombres. Not a Number… Ce sont des NaN.
Les petits, que nous utiliserons. Nous dirons que leur exposant à 0 signifie que le 1 implicite, le chiffre avant la virgule, est remplacé par un 0. Ce sont des nombres finis que nous appellerons des nombres finis dénormalisés.
Rappelons que les valeurs réservées d'exposant 0 et 255 sont données avant décalage. CE sont donc les exposants -127 et +128 dont nous sommes "privés", et non 0, heureusement.
Nous avons maintenant six catégories de nombres :
Les nombres finis normalisés : c'est notre pain quotidien, il n' y a rien à ajouter.
Les deux 0 signés. Si nous considérons qu'un résultat nul dans les réels peut être dû à un problème de capacité de calcul (un underflow), il sera utile d'être renseigné sur le côté par lequel ce résultat a été atteint.
Les deux infinis sont de même nature. Ils signaleront un dépassement de capacité, un overflow.
L'indéfini, quant à lui, sera utilisé pour signaler une circonstance réellement anormale, plus qu'un dépassement de capacité. En particulier quand le coprocesseur ne pourra pas fournir de résultat du tout.
Les NaN : sont situés au-delà du plus grand nombre fini normalisé et de l'infini. Leur apparition au sein d'un calcul est donc un événement grave, le processeur étant censé répondre par un infini à un dépassement de capacité en valeur absolue. L'apparition d'un NaN est une bonne raison de répondre par l'indéfini. Il est fait état dans la norme IEEE de deux groupes de NaN, les QNaN dont le premier bit de la mantisse est à 1 et qui sont autorisés à faire un peu de tourisme dans un calcul, et les SNaN (Signaling NaN) qui déclenchent la foudre de l'exception immédiatement.
Les nombres finis dénormalisés : ont une utilité, puisqu'ils se situent entre le nombre normalisé de plus petite valeur absolue et 0. Ils sont utilisés par la FPU, quand il n'y a pas d'autre solution normalisante. Ils sont néanmoins considérés comme signes d'underflow. Ils sont certainement utilisés dans la cuisine interne du coprocesseur, en vue d'améliorer la précision sur les valeurs normalisées, particulièrement vers les petites valeurs.
Les formats d'entiers de la FPU Intel sont donc les deux formats IEEE sur 32 et 64 bits, et un format 80 bits supplémentaires, l'extended, également décrit par l'IEEE. Ce dernier format offre discrètement sa puissance aux autres. Il dispose en interne d'une précision encore supérieure. Intel divise la mantisse (significand) en une fraction (les bits après la virgule) et un entier, le J-bit (le 1 à priori implicite avant la virgule). Le bit représentant l'entier est un vrai bit en extended.
|
IEEE Single |
IEEE Double |
Extended |
---|---|---|---|
Bits total |
32 |
64 |
80 |
Bits exposant |
8 |
11 |
15 |
Étendue exposant |
-126/+127 |
-1022/1023 |
-16382/+16383 |
Bits mantisse |
23 |
52 |
63 |
Précision (bits) |
24 |
53 |
64 (65) |
La précision est de deux fois celle de la mantisse. En bits, elle s'exprime donc par le nombre de bits de cette mantisse plus 1.
Si nous additionnons les bits de la mantisse, ceux de l'exposant et 1 pour le bit de signe, nous devons obtenir le nombre de bits total. Ce n'est pas le cas pour le format extended, il manque le J-bit.
Ce J-bit va ajouter de la précision, d'où le 65 entre parenthèses. Mais pas sur toute l'étendue des valeurs, uniquement sur les très petites valeurs absolues. Nous avions décidé que l'exposant avant décalage à 0 signifiait un nombre entre 0 et 1, et non plus entre 1 et 2. Avec un vrai J-bit, c'est la même chose, mais 254 fois plus précis, puisque l'exposant peut prendre ce nombre de valeurs.
De ce qui a été vu sur les nombres réels, nous déduisons qu'il y a le domaine du boulier et celui de la règle à calcul, le calcul comptable et le calcul scientifique. En calcul comptable, le résultat doit être rigoureusement exact. En revanche, les données, notamment des quantités monétaires, ont un ordre de grandeur limité et de plus sont des entiers. Nous ne manipulons pas des euros (€), mais des centimes ou des millimes. La virgule est placée ensuite à une position fixe. Nous pourrions penser que la FPU, au vu de son nom, est dédiée au calcul scientifique, sur les pseudo-réels que sont les flottants et inadaptée au calcul exact. C'est faux : en effet, la FPU met à la disposition du programmeur plusieurs types de données et est parfaitement apte à des tâches comptables.
La FPU reconnaît 7 types de données. Il est à préciser, bien que ce point ne fasse pas grande différence pour le programmeur, que ces types n’existent en tant que tels qu’en mémoire. Ils sont transférés dans la FPU comme des Extended de 80 bits. Cette précision signifie surtout que quel qu’en soit le type, donc la taille, une donnée occupe complètement un des 8 registres 80 bits de la FPU. Et que la FPU peut utiliser ces 80 bits pour mener ses calculs et en améliorer la qualité.
Ces 7 types sont à classer en trois catégories :
Trois types de réels.
Trois types d’entiers signés classiques.
Un type BCD (Binary Coded Decimal) signé.
Précisons que le type packed BCD est analogue au type équivalent de la CPU, avec un octet réservé au signe. Cela laisse la place pour 18 chiffres décimaux, 1 bit de signe et 7 bits inutiles, indéfinis.
Avec ce que nous venons de voir sur les réels et ce que nous savons déjà des types entiers et BCD, la représentation suivante suffit à décrire les types de la FPU.
Nous avons vu au cours de la présentation de MASM la façon d'initialiser les valeurs numériques. Rappelons, pour les réels :
; Saisis sous forme de nombres réels:
RS REAL4 25.23 ; format IEEE
RD REAL8 2.523E1 ; format IEEE
RT REAL10 2523.0E-2 ; format Intel réel / 10 bits
; Saisis directement en hexadécimal:
HRS REAL4 3F800000r ; 1.0 en format IEEE short
HRD REAL8 3FF0000000000000r ; 1.0 en format IEEE long
HRT REAL10 3FFF8000000000000000r ; 1.0 en format Intel réel / 10 bits
Il est agréable d'utiliser un TYPEDEF pour par exemple rendre les noms de types compatibles avec ceux auxquels vous êtes habitués, en C/C++ ou Pascal par exemple.
Quand un entier est initialisé en tant que TBYTE et que la base ( RADIX ) est décimale ( t ), l'entier est interprété par MASM comme un packed BCD, ou BCD compacté :
POSIT TBYTE 1234567890 ; codé 00000000001234567890h
NEGAT TBYTE -1234567890 ; codé 80000000001234567890h
La structure réelle de la FPU ne nous intéresse pas, et d'autant moins que, depuis le 8087 séparé jusqu'aux Pentium et autres Athlon en intégrant plusieurs, il n'y a plus de schéma universel. C'est le modèle du programmeur qui va nous occuper au cours du présent paragraphe.
Remarque
Les instructions ESCAPE
Nous avons qualifié, en début de chapitre, d'étranges les rapports entre CPU et FPU. Très schématiquement, et en imaginant plutôt des circuits séparés comme à l’origine, les deux unités ont accès aux bus. Le flux programme est (pré)chargé et au moins en partie décodé par la BIU de la CPU. La FPU va identifier les instructions et les données qui la concernent et va voler ces informations sur le bus, en bloquant au besoin la CPU, faire son travail et enfin rendre la main à la CPU.
Pour faciliter l'identification des instructions FPU, il a été décidé de les faire toutes débuter par 5 bits (les 5 bits de poids fort du premier octet de l'opcode) particuliers : ils sont toujours à 11011, soit 1Bh ou 27. Cette valeur est aussi le code ASCII de l' ESCAPE . Ce fonctionnement est assez analogue aux séquences ESCAPE des périphériques d'affichage ou d'impression : un code particulier qui introduit une séquence interprétée par autre chose .
Donc, si les mnémoniques propres à la FPU débutent tous par la lettre F, le code machine de l’instruction commence toujours par un octet D8 (11011000) à DF (11011111), précédé d’un éventuel préfixe.
Ce comportement est transparent au programmeur, mais vous pouvez trouver les termes escape (ESC) encoding, escape opcode, etc., dans la littérature.
La FPU telle qu’elle intéresse le programmeur peut se schématiser de la façon suivante.
Nous voyons 8 registres de données de 80 bits, qui sont ici représentés comme contenant des extended. Nous avons évoqué le fait que c’est effectivement dans ce type, ou au moins cette taille, de format que sont importés les divers types de données pour subir des calculs.
Nous observons ensuite une série de registres de tailles diverses, dont les noms pour la plupart nous rappellent quelque chose.
Les 8 registres ne sont pas accessibles directement, mais selon la méthode de la pile . Huit registres, donc trois bits, suffisent pour pointer le bon registre. Ce sont les bits 11, 12 et 13, du registre d’état, zone nommée Top Of Stack , qui jouent ce rôle.
Une instruction LOAD est équivalente à un PUSH , et décrémente TOP . Une instruction STORE est équivalente à un POP et incrémente TOP . Un certain nombre d’instructions ont deux versions, dont l’une effectue l’équivalent d’un POP : fmul (multiplie) et fmulp (multiplie et pop).
L'utilisation d'une pile à ce niveau est à rapprocher de la notation polonaise qui a fait (avec leur prix) la réputation des calculatrices HP. Le but est le même : mener des calculs de formules en minimisant les sauvegardes de résultats intermédiaires.
Un dépassement de la capacité de la pile génère une exception FPU stack overflow.
La gestion de cette pile sera un des premiers problèmes et une source d’erreurs potentielle dans la programmation FPU en assembleur.
La pile FPU perdure pendant le changement de procédure. Elle pourra constituer un moyen pratique de passer des paramètres.
Autre élément primordial, le registre d’état (status word). Outre le TOP déjà mentionné, nous trouvons 4 bits C0, C1, C2 et C3, en positions 8, 9, 10 et 14, qui forment un code de condition, analogue à celui que nous connaissons déjà. La différence est que la signification de ces indicateurs va varier avec les instructions. Il faudra donc se reporter à la fiche de l’instruction quand le mnémonique ne suffira pas.
Jusqu’au Pentium Pro, il fallait transférer ces flags dans EFLAGS après positionnement. Il existe sur le Pentium Pro des instructions qu positionnent directement EFLAGS.
Le bit B (busy flag, en position 15) est aujourd’hui inutile, c’est une copie d’ES. Les autres bits indiquent une exception, dont ES, error summary status, en position 7, qui indique qu’une des exceptions a eu lieu, et SF, stack fault flag, en position 6, qui signale une erreur de la pile FPU. De 0 à 5, se trouvent 6 flags correspondant à des exceptions masquables : Invalid Operation, Denormalized Operand, Zero Divide, Overflow, Underflow et Precision.
En face du registre d’état, nous trouvons le registre de contrôle , control word. Les bits 0 à 5 sont justement les masques individuels des exceptions correspondantes du registre d’état.
Les bits 8 et 9 codent la précision (24, 53 ou 64 bits de précision de mantisse, soit l'équivalent de réels sur 32, 64 ou 780 bits) utilisée réellement par la FPU. Il faut à priori toujours laisser la précision par défaut, à 64 bits de mantisse, qui bénéficie à tous les calculs, quelle que soit la taille des données. Dégrader volontairement ce paramètre semble n’avoir pour seul avantage que de reproduire à l’identique le comportement d’autres processeurs, en fait de reproduire leurs erreurs.
Les bits 10 et 11 composent RC, rounding control field, qui paramètre le type d’arrondi (par excès, par défaut, vers 0, au plus près). Le mode par défaut, au plus près, est généralement à conserver.
Le bit 12, infinity control flag, est aujourd’hui obsolète.
Le registre de TAG est un registre dont les 16 bits sont affectés aux 8 registres de données, à raison de 2 bits chacun. Pour chaque registre, l’information stockée ainsi est la suivante :
00 : Donnée valide.
01 : Donnée à 0.
10 : Donnée invalide, anormale ou infinie.
11 : Le registre est vide.
Les trois registres qui restent sont à destination des gestionnaires d'exceptions. Un pointeur sur la dernière instruction utile (qui n'était pas une instruction de contrôle), un autre sur l'opérande de cette instruction et enfin son opcode sont préservés dans trois registres. L'idée peut être de relancer une instruction qui aurait échoué, par exemple. Les 11 bits de l'opcode ne doivent pas nous troubler ; il suffit de leur ajouter les 5 bits du code ESCAPE initial immuable pour retrouver 16 bits.
Il est clair, et ce point avait été annoncé, que le jeu d’instructions de la FPU ne sera pas détaillé comme l’a été le jeu standard.
Les instructions FPU, comme les instructions de ce jeu standard, prennent généralement un ou deux opérandes. Ces opérandes sont situés soit en mémoire, soit dans la pile FPU, mais ne sont jamais une valeur immédiate (si nous excluons les instructions de chargement d’une constante, comme Pi).
L’accès à une donnée en mémoire s’effectue de la même façon que pour les autres instructions : les modes d’adressage.
Les registres de la pile FPU sont accessibles par leur position par rapport au sommet de la pile occupé par ST(0). Il est très fréquent que ce registre soit l’opérande implicite de l’instruction.
Le jeu d'instructions propre à la FPU est ici simplement listé sous forme de tableaux thématiques.
Mnémonique |
Action |
---|---|
FLD |
PUSHe un réel. |
FILD |
PUSHe un entier. |
FBLD |
PUSHe un BCD compacté. |
FST |
Copie un réel de ST vers un autre registre ou la mémoire. |
FIST |
Copie un entier de ST vers un autre registre ou la mémoire. |
FSTP |
POPe un réel de ST vers un autre registre ou vers la mémoire. |
FISTP |
POPe un entier de ST vers un autre registre ou vers la mémoire. |
FBSTP |
POPe un BCD compacté de ST vers un autre registre ou vers la mémoire. |
FXCH |
Échange le contenu de ST et d’un autre registre de donnée (ST(1) par défaut). |
FCMOVcc |
Déplacement conditionnel. Voir tableau suivant. |
Mnémonique |
Action et critère |
---|---|
FCMOVcc |
Déplacement conditionnel. D’un registre vers ST. Instruction récente. |
FCMOVB |
Si plus petit (CF=1). |
FCMOVNB |
Si pas plus petit (CF=0). |
FCMOVE |
Si égal (ZF=1). |
FCMOVNE |
Si différent (ZF=0). |
FCMOVBE |
Si plus petit ou égal ((CF or ZF)=1). |
FCMOVNBE |
Si pas plus petit ni égal ((CF or ZF)=0). |
FCMOVU |
Si pas comparables (PF=1). |
FCMOVNU |
Si comparables (PF=0=). |
Mnémonique |
Constante chargée |
---|---|
FLDZ |
+0.0 |
FLD1 |
+1.0 |
FLDPI |
PI |
FLDL2T |
Log 2 (10) |
FLDL2E |
Log 2 (e) |
FLDLG2 |
Log 10 (2) |
FLDLN2 |
Log e (2) |
Mnémonique |
Opération |
---|---|
FADD /FADDP |
Addition de réels. |
FIADD |
Addition d'un registre et d’un entier en mémoire. |
FSUB / FSUBP |
Soustraction de réels : ST - autre opérande, résultat dans la destination. FSBP : entre ST et un registre, et POP . |
FISUB |
Soustraction : un entier d'un réel : ST - autre opérande, résultat dans la destination. |
FSUBR / FSUBRP |
Soustraction de réels (inverse) : autre opérande - ST, résultat dans la destination. FSUBRP : entre ST et un registre, et POP . |
FISUBR |
Soustraction : un réel d'un entier (inverse) : autre opérande - ST, résultat dans la destination. |
FMUL / FMULP |
Multiplication de réels. FMULP : entre ST et un registre, et POP . |
FIMUL |
Multiplication d'un entier par un réel. |
FDIV / FDIVP |
Division de réels : Divise FP par un autre opérande, résultat dans la destination. FDICP : entre ST et un registre, et POP . |
FIDIV |
Division d'un réel et d’un entier : Divise FP par autre opérande, résultat dans la destination. |
FDIVR / FDIVRP |
Division de réels (inverse) : Divise autre opérande par FP, résultat dans la destination. |
FIDIVR |
Divisons d'un entier et d’un réel (inverse) : Divise autre opérande par FP, résultat dans la destination. |
FABS |
Remplace ST par sa valeur absolue. |
FCHS |
Change le signe de ST. |
FSQRT |
Remplace ST par sa racine carrée. |
FPREM |
Remplace ST par le reste de la division de ST par ST(i) avec quotient entier. Non conforme à IEEE 754. |
FPREM1 |
Remplace ST par le reste de la division de ST par ST(i) avec quotient entier. Conforme à IEEE 754. |
FRNDINT |
Arrondit FP en accord avec le champ RC du registre de contrôle. |
FXTRACT |
Extrait l’exposant de ST (qui le remplace) et la mantisse (qui est empilée). |
Mnémonique |
Action |
---|---|
FCOM / FCOMP / FCOMPP |
Compare des réels. Positionne les flags de code de condition. Puis transfère vers EFLAGS. FCOMP : puis POP . FCOMPP : puis POP deux fois. |
FUCOM / FUCOMP / FUCOMPP |
Compare des réels. Positionne les flags de codes de condition. Puis transfère vers EFLAGS. FUCOMP : puis POP. FUCOMPP : puis POP deux fois. |
FICOM / FICOMP |
Compare des entiers. Positionne les flags de codes de condition. Puis transfère vers EFLAGS. |
FCOMI / FCOMIP |
Compare real and set EFLAGS status flags. positionne les flags de codes de condition. Puis transfère vers EFLAGS. |
FUCOMI / FUCOMIP |
Unordered compare real and set EFLAGS status flags. |
FTST |
Compare ST avec 0.0, positionne les flags de codes de condition. |
FXAM |
Positionne les flags de codes de condition en fonction du type de la donnée dans ST. |
Mnémonique |
Action |
---|---|
FSIN |
Calcule le sinus (de ST dans ST). |
FCOS |
Calcule le cosinus (de ST dans ST). |
FSINCOS |
Calcule le sinus (de ST dans ST) et PUSH le cosinus sur la pile. |
FPTAN |
Calcule la tangente(de ST dans ST). |
FPATAN |
Calcule l’arc tangent (de ST dans ST). |
Mnémonique |
Action |
---|---|
FYL2X |
Calcule log (y * log 2 x) (et POP). |
FYL2XP1 |
Calcule log epsilon (y * log 2 (x + 1)) (et POP). |
F2XM1 |
Calcule (2 x - 1). |
FSCALE |
Ruse mathématique : permet d’effectuer des multiplications par des puissances de 2 très rapidement, en faisant une addition sur l’exposant : ST(1) considéré comme entier est ajouté à l’exposant de ST. |
Mnémonique |
Action |
---|---|
FINIT / FNINIT |
(Ré)initialise la FPU. FINIT vérifie une éventuelle exception, FNINIT ne le fait pas. |
FLDCW |
Charge à partir de la mémoire le registre de contrôle. |
FSTCW / FNSTCW |
Sauve en mémoire le registre de contrôle. FSTSW vérifie une éventuelle exception, FNSTSW ne le fait pas. |
FSTSW / FNSTSW |
Sauve en mémoire ou dans AX le registre d’état. FSTSW vérifie une éventuelle exception, FNSTSW ne le fait pas. |
FCLEX / FNCLEX |
Efface tous les flags concernant les exceptions FPU. FCLEX vérifie une éventuelle exception, FNCLEX ne le fait pas. |
FLDENV |
Restaure le contexte FPU sans les registres de données en inversant le processus de FSTENS / FNSTENV . |
FSTENV / FNSTENV |
Comme FSAVE / FNSAVE sans sauver les registres de données. |
FRSTOR |
Restaure le contexte FPU avec les registres de données en inversant le processus de FSAVE / FNSAVE . |
FSAVE / FNSAVE |
Sauve le contexte complet de la FPU, registres de données compris, vers une zone de 94 ou 108 octets selon le modèle mémoire, et réinitialise la FPU. FSAVE vérifie une éventuelle exception, FNSAVE ne le fait pas. |
FINCSTP |
Incrémente TOP d’une unité. |
FDECSTP |
Décrémente TOP d’une unité. |
FFREE |
Force le Tag du registre de destination à l’état vide. |
FNOP |
Ne fait rien. |
WAIT / FWAIT |
Vérifie et prend en charge d’éventuelles exceptions FPU. Utile pour ne pas exploiter un résultat avant que l’exception ait été traitée. |
Les instructions FPU génèrent leurs propres exceptions, qui vont plus loin que le simple #DE (Divide Error) du fonctionnement normal. Physiquement, ce sont des instances particulières de l'exception 16, #MF (Math Fault). En voici la liste :
Mnémonique |
Nom |
Commentaire |
---|---|---|
#IS |
Stack Overflow or Underflow |
Dépassement de la pile FPU. |
#IA |
Invalid Arithmétic Operation |
Opération invalide (voir référence des instructions). |
#Z |
Floating-Point Divide-by-Zero |
Division FPU par 0. |
#D |
Floating-Point denormal operand |
L'opérande source est un nombre dénormalisé. |
#O |
Floating-Point numeric overflow |
Dépassement du résultat par le haut. |
#U |
Floating-Point numeric underflow |
Dépassement du résultat par le bas. |
#P |
Floating-Point inexact result |
Résultat inexact (voir précision ) |
En assembleur, la notion de réel est liée à la présence soit de la FPU, soit d'une bibliothèque intégrant une émulation de cette unité. En langage de haut niveau comme C/C++ ou Pascal, les réels (type float) sont toujours proposés, et leur format est celui de la norme et/ou de la FPU.
En C++, chaque implémentation est libre de ses formats de réels. C++Builder propose les types de la norme IEEE, plus le format 80 bits, qui sont également les trois types natifs de la FPU x87. Tout va donc pour le mieux. Nous avons trouvé dans l’aide un résumé du format des trois types réels.
D'autres syntaxes, comme Extended, sont acceptées par C++Builder.
La documentation de Pascal Objet (accessible depuis Delphi, mais également depuis C++Builder) est plus généreuse. Voici les types réels, au sens large que donne Delphi à ce terme, disponible dans cet environnement.
Voici également les remarques accompagnant cette page d’aide.
Remarque
Le type Real48
Le type Real48 sur six octets s'appelait Real dans les versions précédentes du Pascal Objet. Si vous recompilez du code utilisant ce type Real sur six octets ancienne manière, vous pouvez le changer en Real48. Vous pouvez également utiliser la directive de compilation {$REALCOMPATIBILITY ON} qui revient à l'interprétation de Real comme un type sur six octets.
Les remarques suivantes s'appliquent aux types réels fondamentaux.
* Real48 est conservé pour la compatibilité ascendante. Comme son format de stockage n'est pas géré naturellement par les processeurs Intel, ce type produit des performances plus mauvaises que les autres types à virgule flottante.
* Extended propose une meilleure précision que les autres types réels, mais il est moins portable. Évitez d'utiliser Extended si vous créez des fichiers de données qui doivent être partagés sur plusieurs plates-formes.
* Le type Comp est un type natif des processeurs Intel et représente un entier sur 64 bits. Il est néanmoins classé parmi les réels car il ne se comporte pas comme un type scalaire. Par exemple, il n'est pas possible d'incrémenter ou de décrémenter une valeur Comp . Comp est conservé uniquement pour la compatibilité ascendante. Utilisez le type Int64 pour de meilleures performances.
* Currency est un type de données à virgule fixe, qui limite les erreurs d'arrondis dans les calculs monétaires. Il est stocké dans un entier sur 64 bits, les quatre chiffres les moins significatifs représentant implicitement les chiffres après la virgule. Quand il est combiné avec d'autres types réels dans des affectations et des expressions, les valeurs Currency sont automatiquement divisées ou multipliées par 10 000.
Ce qui n’est pas explicitement mentionné, mais qui semble plus que probable, c’est que les types Comp et Currency , entiers plus que réels, sont classés avec les réels car ils sont traités dans la FPU, ou éventuellement son émulation logicielle.
Dans ces LHN, l'initialisation se fait de façon intuitive, par la valeur de la donnée réelle. Le séparateur décimal est le point et non la virgule ; il n'est pas indispensable. Attention, en C++, fp0 = 1,2 est compilé, mais n'est bien entendu pas interprété comme attendu : fp0 est initialisé à 1. Il semble impossible d'initialiser par le contenu d'un registre FPU, à moins bien sûr d'utiliser un bloc asm . Sur le programme inclus dans le CD-Rom, quelques essais montrent la différence de précision entre un float et un long double ou Extended .
Un assembleur intégré tel MASM 6 permet de réserver les trois types de réels de la FPU au travers des directives REAL4, REAL8 et REAL10. Pas plus qu’en langage de haut niveau, vous n’êtes tenu de maîtriser le format interne, puisque des initialisations directes par des nombres à point décimal sont autorisées, ce qui est très pratique.
Mais il est également possible d’initialiser un flottant par son contenu réel, exprimé en hexadécimal, à l’aide du suffixe r ou R. Tous les digits de cette constante sont à saisir, en fonction de la taille du réel déclaré. Une vérification de validité est faite par l'assembleur : fp0 REAL4 00000000R est refusé.
Si REAL4, REAL8 et REAL10 n’évoquent rien pour vous, il est courant d'utiliser des directives TYPEDEF pour modifier ce nom, quand elles ne sont pas intégrées dans les includes de votre package.
Pour résumer, examinez cette séquence de déclarations qui se compile sans problème sous MASM 6.14.8444 :
; C/C++
float TYPEDEF REAL4
double TYPEDEF REAL8
long_double TYPEDEF REAL10
; Pascal Objet / Delphi
Single TYPEDEF REAL4
Double TYPEDEF REAL8
Extended TYPEDEF REAL10
; Saisie en réels
;fp4 Single 1. ; accepté
;fp4 Single 1 ; refusé
fp4 Single 1.0
fp8 REAL8 1.233E4
fp10 REAL10 2523.0E-2
; Saisie du contenu du registre
fpdir4 REAL4 3F800000r ; 1
fpdir8 REAL8 3FF0000000000000r ; 1
fpdir10 REAL10 3FFF8000000000000000R ; 1
;fpdir10 REAL10 00000000000000000000R ; refusé
Puisque nous en sommes à la FPU, nous pouvons y ajouter :
packedBCD0 TBYTE 1234567890 ; en mémoire: 00000000001234567890h
packedBCD1 TBYTE -1234567890 ; en mémoire: 80000000001234567890h
Fonctionnera si la base par défaut (directive RADIX) est le décimal (t).
Notre but est de tester quelques instructions FPU primitives, d'utiliser la FPU dans un contexte simple de calcul sur les réels. La petite application que nous allons construire (projet sur le CD-Rom) s’exécute sous Delphi 6 Personnel, Win98se ou Windows XP, et ATHLON XP+. Ces précisions sont importantes, parce que :
Si vous avez installé une version supérieure à Personnel de Delphi ou C++Builder, n'hésitez pas à vous en servir. Vous aurez ainsi la possibilité d'utiliser la fenêtre FPU du débogueur. C'est ce que nous ferons au chapitre suivant. Vous pouvez également compiler le programme en cochant l'option du lieur Informations de débogage TD32 et utiliser ce débogueur.
Nous exploiterons l'instruction CPUID ; nous n'avons testé ce programme que sur la machine décrite. Mais, bien entendu, sans utiliser de spécificité AMD ATHLON.
Dans un premier temps (durant l'événement FormCreate ), nous vérifions la présence d'une FPU et des instructions (F)CMOVcc de transfert conditionnel.
Nous vérifions au préalable la présence de l'instruction CPUID , à l'aide du flag ID en position 21 dans EFLAGS : s'il est modifiable, c'est que CPUID est implantée. En cas de résultat négatif, le programme refuse de continuer.
CPUID est apparue avec le 486 DX4, juste après l'intégration systématique des FPU. Donc, un microprocesseur Intel qui supporterait CPUID mais pas la FPU, s'il existe (un 486SX), serait un collector. Mais il faut bien voir que, justement, CPUID permet une grande souplesse et autorise par exemple un fondeur à créer une version simplifiée de processeur, pour l'embarquer par exemple. L'identification de la présence des MOV conditionnels est cependant souvent utile. De nombreux ordinateurs ne les prenant pas en charge rendent encore de bons et loyaux services. CMOVcc et FCMOVcc sont vérifiées par le même bit. C’est la présence de la FPU plus ce bit qui fournit l’indication pour FCMOVcc . Ces deux vérifications se font par lecture des bits 0 et 15 de EDX, après appel de CPUID avec le numéro de fonction 1 dans EAX. Voici le code de cette partie (nous traiterons en détail de l'identification par ailleurs) :
function TForm1.FormInit():Boolean; register;
asm
push ebx
push edi
mov edi, self.StFondeur
// CPUID supporté ?
// Oui si le bit 21 de EFLAGS peut être modifié
pushfd // EFLAGS dans EAX
pop eax
mov ebx, eax // EFLAGS initial dans EBX
xor eax, 00200000h // Inversion du bit 21
push eax // Actualisation (tentative) de EFLAGS
popfd
pushfd // EFLAGS (nouveau) dans EAX
pop eax
cmp eax, ebx // EFLAGS nouveau = EFLAGS initial ?
je @CPU_pas_bon // Si oui, pas bon
xor eax, eax
cpuid // appel CPUID fonction 0
// pour afficher le nom du fabricant
mov [edi], ebx
mov [edi + 4], edx
mov [edi + 8], ecx
xor eax, eax
inc eax // 1 dans EAX
cpuid // appel CPUID fonction 1
ror dx, 1
setc FPU_Existe // le bit 0 est dans CF
rol dx, 2
setc FCMOVcc_Existe // le bit 15 est dans CF
@CPU_bon:
mov eax, 1
jmp @fin
@CPU_pas_bon:
xor eax, eax
jmp @fin
@fin:
pop edi
pop ebx
end;
Peu de remarques sur cette fonction, écrite en assembleur pur.
C'est une méthode de la classe TForm1 (notre application). C'est simplement un peu plus logique. Nous avons ainsi pu faire de la chaîne StFondeur une propriété (private) de la classe TForm1 . Ce qui permet de résoudre de petits problèmes de droits d’accès et surtout d’appliquer une technique supplémentaire. La syntaxe de la déclaration, à insérer dans le bloc de déclaration de la classe de l’application :
type
TForm1 = class(TForm)
LblFondeur: TLabel;
BtnCalcule: TButton;
..
..
procedure BtnCalculeClick(Sender: TObject);
function FormInit():Boolean;register;
private
StFondeur: AnsiString;
public
..
end;
La récupération de la propriété dans la méthode s’effectue simplement (et lisiblement) :
mov edi, self.StFondeur
Qui se désassemble en mov edi, [eax+$00000324] . $324 est l’offset de StFondeur dans son instance. Attention, self est toujours remplacé par EAX. Il est donc dangereux de l’utiliser en dehors du tout début de la routine.
Le retour (valeur booléenne) se fait dans AL, puisque nous avons utilisé l'attribut register .
Nous allons résoudre une équation du second degré (coefficients et résultats réels), ce qui n'est pas très original. Cette équation consiste à trouver les valeurs de X si elles existent telles que :
A*X
2
+ B*X + C = 0.
La méthode apprise au lycée est de calculer Delta = B2 - 4*A*C . Si cette valeur est négative, il n'y a pas de solution réelle. Si elle est nulle, il y a une solution, dite racine double, qui vaut -B/2A. Si elle est strictement positive, il y a deux solutions distinctes :
R1 = (-B + racine_carrée(Delta))/(2*A) et
R2 = (-B - racine_carrée(Delta))/(2*A).
Notre approche sera la suivante :
1 Coder en Pascal, pour faire facilement un cadre de travail et tester certains choix et comportements des variables réelles.
2 Espionner Delphi à l'aide du débogueur pour voir l'utilisation qui est faite de la FPU.
3 Recoder certaines parties à l'aide d'inclusions d'assembleur.
C'est une bonne méthode d'apprentissage. Malheureusement, elle est inapplicable aux instructions que nous verrons au chapitre suivant. Remarquez, ce n'est pas plus mal comme cela ; sinon, à quoi servirait l'assembleur inline ?
Le code commencera par :
var
A, B, C: Single;
Delta, Racine1, Racine2: Extended;
begin
A := StrToFloat(EdA.Text);
B := StrToFloat(EdB.Text);
C := StrToFloat(EdC.Text);
A, B et C sont saisis à la main. Donc, inutile de prévoir une précision de 80 bits. Il faudra être prudent, la saisie n'est pas sécurisée et taper son nom de famille par exemple génère une exception. Sauf si on s’appelle Marcel 123,54. Delphi propose des solutions élégantes à ce petit problème, mais ce n'est pas ici notre préoccupation.
Il est souvent, à juste titre, déconseillé de tester l'égalité de deux réels. Par exemple, dans quelle mesure 1+1 en single et en extended sont-ils égaux ? D'un autre coté, il semble que la FPU ait amélioré cet aspect. D'où un premier test :
A := 1;
Delta := 1;
if Delta = A then ShowMessage('Delta = A !');
Puis, après avoir saisi A = 1, B = 2 et C = 1 :
Delta := (B * B) - (4 * A * C);
if Delta = 0 then ShowMessage('Delta = 0 !');
Dans les deux cas, l'égalité est détectée. Il faudra néanmoins rester prudent.
Une bonne précaution, en Pascal comme en assembleur ou en C++ : tester les blocs avant de passer aux opérations plus compliquées. Les ShowMessage() pourront ensuite être effacés ou, mieux, mis en commentaire ou modifiés :
begin
A := StrToFloat(EdA.Text);
B := StrToFloat(EdB.Text);
C := StrToFloat(EdC.Text);
if A = 0 then begin
ShowMessage('A ne peut être nul!');
Exit;
end;
Delta := (B * B) - (4 * A * C);
if Delta < 0 then begin
ShowMessage('Delta négatif !');
Exit;
end
else begin
ShowMessage('Delta positif ou nul');
if Delta = 0 then begin
ShowMessage('Delta nul');
end
else begin
ShowMessage('Delta positif');
end;
end;
ShowMessage('Fin Procedure');
end;
Il est temps de coder les formules dans ce cadre :
else begin
if Delta = 0 then begin
Racine1 := -B / (2 * A);
ShowMessage('Une racine double: ' + FloatToStr(Racine1));
end
else begin
Racine1 := (-B + Sqrt(Delta)) / (2 * A);
Racine2 := (-B - Sqrt(Delta)) / (2 * A);
ShowMessage('Deux racines réelles R1= '
+ FloatToStr(Racine1)
+ ' et R2= '
+ FloatToStr(Racine2));
Mettre un point d'arrêt et taper 1, 0 et -4 comme coefficients (X2 - 4 = 0 a bien sûr 2 et -2 comme racines) :
Que voyez-vous ? Que malgré la complexité apparente (et bien réelle) du jeu d'instructions, le compilateur Delphi/BASM utilise la FPU comme une calculatrice scientifique à pile (opérationnelle, pas électrique). Il retravaille l'ordre de calcul dans la formule, comme nous l'aurions fait. Il serait intéressant de voir si les parenthèses inutiles dont nous sommes friands ont une influence sur le code généré.
Remarquez que trois instructions sont des versions avec POP . Il sera intéressant de suivre ce passage à l'aide d'un débogueur muni d'une fenêtre FPU. Le débogueur n'est pas la seule solution pour avoir accès au code machine produit : soit à l'aide des options (selon la version), soit par l'intermédiaire de la ligne de commande et de ses options, il suffit de générer un fichier listing xxxxx.lst .
Nous allons simplement remplacer le code proposé par du code de notre main, mais pas vraiment de notre cru :
//Racine1 := (-B + Sqrt(Delta)) / (2 * A);
asm
fld Delta
fsqrt
fld B
fchs
faddp
fld deux
fmul A
fdivp
fstp Racine1
wait
end; //asm
Nous avons dû auparavant déclarer :
const
deux: Single = 2;
Le WAIT (ou FWAIT ) n’est semble-t-il pas utile dans les processeurs modernes. Le programme se comporte de la même façon avec et sans, sur une DivideError par exemple. Néanmoins, s'il est important que l'instruction suivant une instruction générant une exception ne soit pas exécutée avant que l'exception ne soit traitée, il est bon d'insérer un WAIT (ou un FNOP , peut-être plus rapide).
Déclarer deux: Integer = 2; fonctionnera également, il faudra alors utiliser fild deux . ST(1) est implicite sous BASM dans fadd(p) et fdiv(p) . Il n’est pas rare que BASM refuse une syntaxe utilisée par le désassembleur du débogueur.
Le sommet de la pile (ST ou ST(0)), situé en bas sur le schéma, est ainsi occupé, pour chaque instruction.
Il n’y a au maximum que 2 niveaux de pile sur 8 utilisés. Ceux qui ont inventé ce truc avaient oublié d’être stupides. Nous en venons à regretter que le sommet de la pile CPU (la pile normale) ne puisse être utilisé comme un registre de destination. Mais il est vrai qu’il existe peu d’instructions du jeu normal agissant sur une seule donnée, comme NOT , NEG ou FSQRT , ce qui fait perdre de l’intérêt à la méthode.
Nous pourrions craindre des interactions de la FPU gérée par Delphi et nos propres accès en asm . Nous avons testé le code :
Racine1 := (-B + Sqrt(Delta)) / (2 * A);
asm
finit
end; //asm
Racine2 := (-B - Sqrt(Delta)) / (2 * A);
Aucun problème pour Delphi avec ce test pourtant violent.
Voilà, à partir de ce canevas et de la liste d'instructions, il sera facile de mener à bien des tests. Il est souvent plus facile de regarder exactement ce que fait une instruction que de décoder la documentation. Il est alors éventuellement judicieux de dresser une fiche à partir de ces expériences.