l'assembleur  [livre #4061]

par  pierre maurette




La FPU

Nous présentons dans ce chapitre et le suivant les améliorations apportées au processeur de base : FPU, MMX, 3DNow! et SSE. Ce regroupement est justifié par le fait que la technique est à chaque fois la même : ajout de quelques registres et d'un certain nombres 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 tpout cas de cette façon qu'elles nous apparaissent. Voyons rapidement le contexte historique de leurs apparitions.

Coprocesseur arithmétique, FPU comme 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 en assembleur, qui devait par contre se préoccuper de la présence ou de l'absence de l'extension.

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, on peut également imaginer un émulation totale, instruction par instruction, avec maintien des flags et d'une copie des registres en mémoire. Il faut également émuler les exception 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 rajoute 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.

8.1 Nombres réels et virgule flottante

Quelques notions de mathématiques s’imposent avant de commencer l'étude de la FPU. Les initiés peuvent sauter ce paragraphe, 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 bizarres. Disons pour simplifier qu'il s'agit des nombres à virgule, avec autant de chiffre que l'on veut 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 : on les appelle 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 .

L'important est bien de voir que 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 bonne pour préparer 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 400000 valeurs différentes, de 0 à 99999, 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 100000. C'est moins bien. Beaucoup moins bien.

Nous voyons que la précision varie avec la taille du nombre (on parle 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.

8.2 Représentation des réels en informatique

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 70 une préoccupation importante en pourcentatge des utilisateurs d'ordinateurs. Ce n'est par hasard que Fortran (Formula Translato) faisait partie des tous 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 faire se peut 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 : deux objets A et B, à une distance de l'ordre de 36000 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/100 (0,001) du décimal deviennent de 1/2 (0,5), 1/4 (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 introduisant l'assembleur intégré, 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 :

Structure de Float
figure 8.01 Structure de Float

  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 du paragraphe, 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 à 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 (ce ferait 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, et nous nous retrouvons avec à notre disposition la bagatelle de 2 * 2 * 2 23 = 2 25 (le premier 2 pour les deux valeurs du signe, le second pour les deux valeurs de l'exposant) ce qui est énorme. Servons-nous, en parant au plus urgent :

0 00000000 00000000000000000000000 :  0 (le vrai)

1 00000000 00000000000000000000000 : -0 (schismatique et dissident, mais bien utile)

 

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. Même pô un nombre. Not a Number… Ce sont des NaN.

  Les petits, si mignons avec leurs exposants à 0. Ce sont des nombres finis, après tout. Simplement, ils ne font pas pour l'instant partie des projets de l'entraîneur. Nous les appellerons des nombres finis dénormalisés.

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 coté par lequel ce résultat a été atteint.

  Les deux infinis sont de même nature. Ils signalerons un dépassement de capacité, un overflow.+ l'infini

  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 : Ils 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 : ils ont de toute évidence 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.

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émentaire, l'extended. Mais n'est-il pas devenu une norme de fait ? 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 1 a priori implicite avant la virgule). Le bit représentant l'entier est un vrai bit en extended.

Les formats réels de la FPU Intel

 

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

 

8.3 La FPU

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 d'euro. 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 elle est parfaitement apte à des tâches comptables.

8.4 Les types de données

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.

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 :

Les données de la FPU
figure 8.02 Les données de la FPU [the .swf]

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 exempl 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

 

8.5 La structure de la FPU

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 :

Structure générale de la FPU
figure 8.03 Structure générale de la FPU [the .swf]

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 le 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.

La pile FPU
figure 8.04 La pile FPU [the .swf]

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 de 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 a 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.

8.6 Les instructions FPU

Généralités

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 l’on exclut 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.

8.6.1 Le jeu d'instructions

Le jeu d'instructions propre à la FPU est ici simplement listé sous forme de tableaux thématiques.

Instructions de déplacement de données

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.

 

Instructions de déplacement conditionnel de données

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=).

 

Instructions de chargement de constantes

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)

 

Instructions arithmétiques primitives

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 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 le division de ST par ST(i) avec quotient entier. Non conforme à IEEE754.

FPREM1

Remplace ST par le reste de le division de ST par ST(i) avec quotient entier. Conforme à IEEE754.

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).

 

Instructions de comparaison

Mnémonique

Action

FCOM / FCOMP / FCOMPP

Compare des réels. Positionne les flags de codes 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.

 

Instructions trigonométriques

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 tangente (de ST dans ST).

 

Instructions exponentielles et logarithmiques

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.

 

Instructions de contrôle

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.

 

8.6.2 Les exceptions

Les instructions FPU génèrent leurs propres exceptions, qui vont plus loin que le simple #DE, Divide Error du fonctionnement normal. Physiqement, ce sont des instances particulières de l'exception 16, #MF, Math Fault. En voici la liste :

Les exceptions FPU

Mnémonique

Nom

Commentaire

#IS

Stack Overflow or Underflow

Dépassement de la pile FPU.

#IA

Invalid Arithmétc 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'operande 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 )

 

 

8.7 Les données FPU des compilateurs et assembleurs

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 :

Réels sous C++ Builder 6
figure 8.05 Réels sous C++ Builder 6

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 le types réels, au sens large que donne Delphi à ce terme, disponible dans cet environnement :

Réels sous Delphi 6
figure 8.06 Réels sous Delphi 6

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 10000.

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).

8.8 Travaux pratiques

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. Beaucoup d'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 le 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 un 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 . Le 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 :

Coder en Pascal, pour faire facilement un cadre de travail et tester certains choix et comportements des variables réelles.

Espionner Delphi à l'aide du débogueur, pour voir l'utilisation qui est faite de la FPU.

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) :

Espionnage
figure 8.07 Espionnage

Que voit-on ? 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 .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 :

Évolution de la pile FPU
figure 8.08 Évolution de la pile FPU [the .swf]

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ée 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.

Vous trouverez dans les premiers paragraphes du chapitre sur l'optimisation deux exemples de code FPU simples (Opti8) et (Opti9), accompagnés du même schéma d'évolution de la pile. Vous pouvez vous y reporter tout de suite.

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.

 

 

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1