Skip to content

Niveau 5 · Chapitre 5.6

Les types que comprend le matériel

Les formats que supposent réellement les instructions d’un CPU : entiers signés et non signés en complément à deux et la façon dont les instructions les distinguent, le décimal codé binaire et sa disparition du x86 64 bits, la virgule flottante IEEE 754 avec un explorateur interactif bit à bit (fp64, fp32, fp16, bfloat16), les caractères et l’UTF-8, les tables de bits et les pointeurs.

Un type de données a un support matériel quand une instruction attend des données dans ce format. Le logiciel peut inventer toutes les représentations qu’il veut — un entier de 256 bits, une date, un nombre rationnel —, mais seuls les formats pris en charge par le matériel ont des instructions qui les traitent directement, en une étape. Tout le reste est construit en logiciel à partir d’eux. Ce chapitre passe en revue les formats que les CPU prennent réellement en charge : entiers, décimal, virgule flottante, caractères, bits et pointeurs.

Le chapitre sur les variables a regardé ces types depuis le C : tailles, signe, ordre des octets. Ici, la question est ce que supposent les instructions elles-mêmes.

Les entiers : les mêmes bits, deux lectures

Toutes les ISA courantes prennent en charge des entiers de 8, 16, 32 et 64 bits, non signés et signés, les signés étant en complément à deux : pour prendre l’opposé d’un nombre, on inverse tous ses bits et on ajoute 1. Un octet contient 0 à 255 en non signé, ou −128 à 127 en signé. 1111 1111 vaut 255 ou −1, selon ce qu’on veut dire.

Le complément à deux l’a emporté sur les autres représentations parce que l’addition ne fait pas la différence. Additionner les motifs de bits donne le bon résultat qu’on les lise comme signés ou non signés, donc un seul additionneur et une seule instruction add servent aux deux, et la soustraction est l’addition de l’opposé. Des machines plus anciennes utilisaient le signe et valeur absolue (un bit de signe plus la valeur absolue) ou le complément à un (l’opposé inverse simplement les bits) ; les deux ont deux zéros, +0 et −0, et demandent un matériel séparé pour l’arithmétique signée. La norme C23 les a enfin abandonnés : les entiers du C sont désormais obligatoirement en complément à deux.

Puisque les bits ne disent pas quelle lecture est voulue, c’est l’instruction qui le dit. Les opérations où signé et non signé diffèrent vont par paires :

OpérationNon signéSigné
élargir vers un registre plus grandmovzx (extension par des zéros)movsx (extension du signe : recopier le bit de poids fort)
comparer et brancherjb, ja (inférieur/supérieur : CF)jl, jg (plus petit/plus grand : SF et OF)
indicateur de débordementCF = le résultat ne tient pasOF = le résultat ne tient pas
décalage à droiteshr (fait entrer des zéros)sar (fait entrer des copies du bit de signe)
multiplication, divisionmul, divimul, idiv
Live · Un octet, deux lectures

À essayer : Appuyez sur Step pour exécuter une instruction, Run pour animer ou Continue pour aller au bout ; les boutons L2 à L7 changent de niveau, vers le bas ou le haut.

program— ▸ is the next instruction
  1. mov al, 0xFF ; the byte 1111 1111
  2. movsx ebx, al ; read as signed: -1
  3. movzx ecx, al ; read as unsigned: 255
  4. mov dl, 200
  5. add dl, 100 ; 300 needs 9 bits: dl = 44, CF = 1
  6. mov sil, 100
  7. add sil, 100 ; 200 > 127: signed overflow, OF = 1
step 0
Loading emulator…
The instruction that just ran, as the bytes the CPU actually fetched and decoded.

movsx remplit ebx de copies du bit de poids fort, 0xFFFFFFFF, soit −1. movzx remplit de zéros : 255. La première addition positionne la retenue, le débordement non signé. La seconde positionne à la place l’indicateur de débordement : 200 tient dans un octet en non signé, mais en signé, 100 + 100 a fait le tour et vaut −56. Chaque add positionne les deux indicateurs ; c’est le programme qui choisit lequel regarder, comme l’a montré le chapitre sur les indicateurs.

Le complément à deux n’est pourtant pas le seul codage d’entiers encore utilisé. La notation excédent k (ou biaisée) range une valeur v sous la forme v + k : avec k = 127, l’octet 0111 1111 signifie 0. Son avantage est que les grandes valeurs ont de grands motifs de bits, même négatives, et se comparent donc comme des entiers non signés. C’est exactement ainsi que sont rangés les exposants des nombres flottants, comme on va le voir.

Le décimal : le DCB

Le décimal codé binaire (DCB, ou BCD en anglais) range un chiffre décimal sur 4 bits, deux par octet : 42 s’écrit 0100 0010, soit 0x42. C’est du gaspillage — un octet contient 0 à 99 au lieu de 0 à 255 —, mais la conversion vers le texte et depuis le texte est triviale, et les fractions décimales comme 0,10 sont exactes, ce qui compte pour les comptables et les programmes COBOL. L’addition binaire donne de faux résultats sur des chiffres DCB (0x09 + 0x01 donne 0x0A, pas 0x10), donc les machines qui prennent en charge le DCB ont des instructions d’ajustement décimal qui corrigent le résultat, à l’aide de la retenue sortant du bit 3 — l’indicateur de retenue auxiliaire que le x86 calcule encore à chaque addition.

Tanenbaum range le DCB parmi les types du Core i7. C’est vrai, mais seulement en mode 32 bits. Les instructions d’ajustement décimal du x86 (DAA, DAS, AAA, AAS, AAM, AAD) ont été retirées du mode 64 bits, et leurs opcodes réaffectés : nasm assemble daa en l’octet 0x27 en mode 32 bits, et le refuse en mode 64 bits avec instruction not supported in 64-bit mode. Ce qui subsiste, ce sont les instructions FBLD et FBSTP de l’unité flottante x87, qui chargent et rangent un nombre DCB compact de 18 chiffres. ARM et RISC-V n’ont jamais eu de DCB. Les machines qui prennent encore le décimal au sérieux sont les grands systèmes et les processeurs POWER d’IBM, qui implémentent en matériel la virgule flottante décimale — les formats décimaux d’IEEE 754.

La virgule flottante

Pour les nombres non entiers, les CPU utilisent la virgule flottante : un nombre écrit ± significande × 2^exposant, l’équivalent binaire de la notation scientifique. Depuis 1985, pratiquement tous les CPU suivent la norme IEEE 754, qui fixe les formats bit à bit, ainsi que l’arrondi — si bien qu’un calcul donne le même résultat sur toute machine conforme.

Un nombre flottant binaire a trois champs :

  • un bit de signe ;
  • un exposant, rangé en notation excédent, avec un biais de 127 en simple précision ;
  • une fraction : les bits du significande après la virgule. Pour les nombres normaux, le significande vaut 1,fraction : le 1 de tête n’est pas stocké, puisqu’un nombre binaire normalisé commence toujours par 1. C’est le bit caché, un bit de précision gratuit.

L’explorateur ci-dessous range un nombre dans chaque format. Tapez une valeur, ou cliquez sur un bit pour l’inverser :

Floating point · Comment 0,1 est rangé

À essayer : Tapez un nombre ou cliquez sur un exemple, changez de format (en haut à droite), ou cliquez sur un bit pour l’inverser.

1 sign bit8 exponent bits (bias 127)23 fraction bitsclick a bit to flip it
0x3dcccccd
stored as
32 bits, float (binary32)
normal
kind
0.1
reads back as
shortest decimal that round-trips
rounded
typed value
the nearest representable value
sign
0 → positive
exponent
123 − 127 = -4
significand
1.fraction = 1.60000002384185791015625
value
+1.60000002384185791015625 × 2^-4 = 0.100000001490116119384765625

Rounding is round-to-nearest, ties-to-even, as in hardware. Sums like 0.1+0.2 are computed in double precision first, as a program would, then stored in the chosen format.

0,1 n’a pas de représentation binaire finie : comme 1/3 en décimal, ses chiffres binaires se répètent à l’infini (0,000110011001100…). Le format garde les 24 premiers bits significatifs et arrondit, donc ce qui est réellement rangé dans un float vaut 0,100000001490116119384765625. Passez en fp64 et l’erreur tombe à environ 5,6 × 10⁻¹⁸, mais elle ne disparaît pas. C’est pourquoi, en double précision, 0.1 + 0.2 donne 0,30000000000000004 et pas 0,3 : les deux opérandes sont déjà arrondis, et leur somme aussi. Essayez 0.1+0.2 dans l’explorateur.

Le même arrondi limite les nombres entiers : un float a 24 bits de significande, donc au-delà de 2²⁴ = 16 777 216, tous les entiers ne tiennent pas. Tapez 16777217 : il est rangé comme 16777216. Un double a 53 bits, ce qui explique que le plus grand entier représentable exactement en JavaScript soit 2⁵³ − 1.

La norme réserve les exposants extrêmes aux valeurs spéciales :

  • exposant nul, fraction nulle : ±0 — il existe un zéro négatif, qui est égal à +0 dans les comparaisons ;
  • exposant nul, fraction non nulle : les nombres dénormalisés (subnormaux), sans 1 caché, qui comblent l’écart entre le plus petit nombre normal et zéro en perdant progressivement de la précision (essayez 1e-40 en fp32) ;
  • exposant tout à un, fraction nulle : ±∞, le résultat d’un dépassement ou d’une division d’un nombre non nul par zéro ;
  • exposant tout à un, fraction non nulle : NaN, not a number, le résultat de 0/0 ou de √−1. Un NaN est différent de tout, y compris de lui-même.

Les formats

FormatBitsExposantFractionPlus grande valeur finieChiffres décimaux
binary64 (double)6411521,8 × 10³⁰⁸~16
binary32 (float)328233,4 × 10³⁸~7
binary16 (demi-précision)1651065 504~3
bfloat1616873,4 × 10³⁸~2

Le livre donne des longueurs flottantes de 32, 64 et 128 bits. Depuis, l’apprentissage automatique a poussé dans l’autre sens : la demi-précision (fp16) et le bfloat16 de Google sont pris en charge en matériel par les x86 récents (AVX-512 FP16 et BF16), par ARM64 et par les GPU, et les accélérateurs d’IA descendent à 8 et même 4 bits. Le bfloat16 est simplement un float amputé de ses 16 bits de fraction les plus faibles : même exposant de 8 bits et même étendue, bien moins de précision — ce dont l’entraînement des réseaux de neurones a justement besoin. Passez l’explorateur de fp16 à bf16 sur 65504 ou 0.1 pour voir le compromis. À l’autre bout, la quadruple précision sur 128 bits est surtout réalisée en logiciel ; le x86 garde aussi le format étendu sur 80 bits du x87.

La virgule flottante a en général ses propres registres : xmm0–xmm15 sur x86-64 (l’ancien x87 avait une pile de 8 registres), v0–v31 sur ARM64, f0–f31 sur RISC-V avec ses extensions F et D.

Les caractères

Un caractère est rangé sous la forme d’un nombre, son point de code. L’ASCII attribue 128 caractères sur 7 bits. Tanenbaum décrit l’Unicode comme un code sur 16 bits, ce qui était sa conception d’origine. C’est dépassé depuis 1996, quand Unicode a dépassé 65 536 caractères : les points de code vont désormais jusqu’à U+10FFFF, un espace de 21 bits de 1 114 112 valeurs. On le range dans l’un de plusieurs codages :

CaractèrePoint de codeOctets UTF-8UTF-16
AU+0041410041
éU+00E9c3 a900e9
€U+20ACe2 82 ac20ac
😀U+1F600f0 9f 98 80d83d de00 (une paire de substitution)

L’UTF-8 utilise 1 à 4 octets par caractère et coïncide avec l’ASCII pour les 128 premiers, ce qui explique qu’il ait conquis les fichiers et le Web. L’UTF-16, utilisé à l’intérieur de Windows, de Java et de JavaScript, a besoin de deux unités de 16 bits, une paire de substitution, pour les caractères au-delà de U+FFFF.

Au niveau de l’ISA, les caractères ne sont que des octets : aucun CPU courant ne connaît l’UTF-8. Le x86 a en revanche des instructions de chaînes qui travaillent sur des suites d’octets : rep movsb copie un bloc, repne scasb cherche un octet, et pcmpistri, de SSE4.2, compare 16 octets à un ensemble de caractères en une instruction. ARM et RISC-V n’ont rien de tout cela, et laissent les chaînes aux chargements, rangements et instructions vectorielles ordinaires.

Bits et pointeurs

Un booléen n’a besoin que d’un bit, mais un bit isolé n’a pas d’adresse, donc les langages rangent chaque booléen dans un octet. Quand ils sont nombreux — une table de bits des blocs libres d’un disque, l’ensemble des pages touchées par un programme —, on les tasse 64 par mot, et l’ISA a des instructions pour cela : les bt/bts du x86 testent et positionnent le bit n, popcnt compte les bits à 1 d’un mot, et tzcnt/lzcnt trouvent le premier ou le dernier. ARM64 et RISC-V ont des équivalents.

Un pointeur est un entier non signé aussi large qu’une adresse. Le matériel le traite comme n’importe quel entier jusqu’à ce qu’il serve à adresser la mémoire. Comme les CPU 64 bits n’utilisent que 48 à 57 bits d’adresse, les ISA récentes ont commencé à exploiter le reste : le top-byte ignore d’ARM64 permet au logiciel de ranger une étiquette dans les 8 bits de poids fort, et l’authentification de pointeurs y range une signature cryptographique, vérifiée avant l’usage du pointeur, ce qui déjoue beaucoup d’attaques par corruption de mémoire. Le x86 ajoute une fonction voisine, le linear address masking.

Qui prend en charge quoi

x86-64ARM64RISC-V (RV64GC)AVR ATmega168
entiers8, 16, 32, 648, 16, 32, 64 (arithmétique en 32 et 64, chargements/rangements de toutes tailles)8, 16, 32, 64 (arithmétique en 32 et 64)8 (plus des pointeurs de 16 bits)
DCBmode 32 bits seulement ; DCB compact du x87———
virgule flottante16 (récent), 32, 64, 80 du x8716, 32, 6432, 64 (16 avec Zfh)—
instructions de chaînesoui———

La colonne ATmega168 vient du livre : un microcontrôleur dont le seul vrai type de données est l’octet. La colonne ARM s’écarte encore sur un point de l’OMAP4430 32 bits du livre : celui-ci affirme que tous ses opérandes doivent être alignés, mais les chargements et rangements ordinaires de l’ARM 32 bits acceptent les adresses mal alignées depuis ARMv6, comme l’ont montré sur ARM64 les mesures du chapitre précédent.

À retenir

  • Un type a un support matériel quand des instructions supposent son format ; tout le reste est construit en logiciel.
  • Les entiers sont en complément à deux : un seul additionneur pour le signé et le non signé. Les bits ne disent pas lequel ; c’est l’instruction qui le dit — movsx/movzx, jl/jb, sar/shr, imul/mul, OF/CF.
  • Le DCB range des chiffres décimaux sur 4 bits. Le x86 a abandonné ses instructions d’ajustement décimal en mode 64 bits ; les machines IBM gardent une virgule flottante décimale en matériel.
  • Virgule flottante IEEE 754 : signe, exposant biaisé, fraction avec un 1 caché ; valeurs spéciales ±0, dénormalisés, ±∞, NaN. 0,1 est toujours arrondi, et un float ne peut pas contenir 16 777 217.
  • Le fp16 et le bfloat16 sont désormais en matériel pour l’apprentissage automatique ; le bfloat16 garde l’étendue d’un float avec bien moins de précision.
  • Unicode est un espace de 21 bits, pas de 16 ; l’UTF-8 le code sur 1 à 4 octets. Les CPU ne voient que des octets, avec quelques instructions de chaînes sur x86.
  • Les pointeurs sont des entiers de la taille d’une adresse ; ARM64 range des étiquettes et des signatures dans leurs bits de poids fort inutilisés.

Dans ce niveau

  1. 5.1Ce que garantit l’ISA : modèle mémoire, alignement et ordre
  2. 5.2Formats et encodage des instructions
  3. 5.3Modes d’adressage
  4. 5.4x86, ARM et RISC-V côte à côte
  5. 5.5Déroutements, interruptions et exceptions
  6. 5.6Les types que comprend le matériel
  7. 5.7VLIW, EPIC et l’Itanium