Bases, entiers, complément à deux, bits, flottants IEEE~754
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
Les bits derrière les valeurs du C : comment un entier, signé ou non, et un réel s’écrivent en binaire sur un nombre fixe de bits, ce que deviennent ces bits quand le type change ou qu’un calcul déborde, et comment un programme les manipule un par un.
float IEEE 754 (signe, exposant biaisé, mantisse), le lire dans un dump little-endian, et expliquer \(0{,}1 + 0{,}2 \neq 0{,}3\).D’où l’on vient, où l’on va
Amont. Séance 1 : tailles des types sur x86-64 et conversions implicites, avec des résultats laissés sans explication : \(-56\) pour (char)200, \(-25536\) pour (short)40000, 4294967295 pour (unsigned)-1, \(-2147483648\) pour INT_MAX + 1.
Cette séance. Les motifs de bits qui donnent ces résultats : bases, entiers non signés et complément à deux, conversions, débordements, opérations bit à bit, flottants IEEE 754.
Aval. Séance 3 : où ces octets sont rangés : adresses, endianness, pointeurs, tableaux, pile et tas.
Une variable C occupe un nombre fixe de bits ; avant de parler de signe ou de virgule, il faut savoir ce que vaut une suite de 0 et de 1.
Notation binaire
En base 2, la position \(i\) pèse \(2^i\) :
\[ b_{n-1}\dots b_1 b_0 \;=\; \sum_{i=0}^{n-1} b_i\, 2^i, \qquad b_i \in \{0, 1\} \]
Le bit de poids fort (MSB, {most significant bit}) est à gauche, le bit de poids faible (LSB) à droite.
Exemple
\(1011_2 = 1\cdot 8 + 0\cdot 4 + 1\cdot 2 + 1\cdot 1 = 11\).
Décimal vers binaire
Diviser par 2 tant que le quotient n’est pas nul ; les restes, lus du dernier au premier, sont les bits du poids fort au poids faible : \(13 \rightarrow 1101_2\).
Lecture. \(n\) bits donnent \(2^n\) motifs : 256 sur un octet. Le programme suivant applique les divisions successives à 186.
Valeurs relevées sous gdb à chaque tour ; en rouge, le reste écrit dans b[i].
Sortie \(\rightarrow\)
186 = 10111010
| hex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| déc. | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| bin. | 0000 |
0001 |
0010 |
0011 |
0100 |
0101 |
0110 |
0111 |
1000 |
1001 |
1010 |
1011 |
1100 |
1101 |
1110 |
1111 |
Binaire et hexadécimal
Grouper les bits par 4 depuis la droite : \(1011\,1010_2 \rightarrow\) b, a, soit 0xba. Un octet s’écrit donc toujours en deux chiffres hexadécimaux (%02x), de 00 à ff ; c’est la notation des dumps.
Résultat \(\rightarrow\)
decimal : 186
hexadecimal : 0xba 0xBA
octal : 0272
chiffres : b puis a
litteraux : 186 186 186
Lecture. En C, 0x introduit un littéral hexadécimal, un 0 en tête un littéral octal (0272), et %x, %X, %o les affichent. Les quatre bits de poids fort de 186 donnent b (\(11\)), les quatre de poids faible a (\(10\)).
Un type entier de \(w\) bits offre \(2^w\) motifs ; un type non signé les fait tous correspondre à des valeurs positives ou nulles.
Non signé sur \(w\) bits
\[ B2U_w(\vec{x}) = \sum_{i=0}^{w-1} x_i\, 2^i \]
Intervalle : \([0,\; 2^w - 1]\) ; aucune valeur négative. \(\texttt{0xdb} = 1101\,1011_2 = 219\).
Résultat \(\rightarrow\)
0xdb = 219
unsigned char : 0 .. 255
unsigned short : 0 .. 65535
unsigned int : 0 .. 4294967295
unsigned long : 0 .. 18446744073709551615
Lecture. <limits.h> donne les maximums de la machine du cours : \(2^8-1\), \(2^{16}-1\), \(2^{32}-1\) et \(2^{64}-1\) (unsigned long mesure 8 octets en LP64).
Quoi
Le bit de poids fort pèse \(-2^{w-1}\) au lieu de \(+2^{w-1}\) :
\[ B2T_w(\vec{x}) = -x_{w-1}\, 2^{w-1} + \sum_{i=0}^{w-2} x_i\, 2^i \]
Ce bit est le bit de signe (1 : négatif). Intervalle : \([-2^{w-1},\; 2^{w-1}-1]\), soit \([-128, 127]\) sur 8 bits.
Pourquoi
L’addition est la même que pour les non signés (modulo \(2^w\)) : le processeur n’a qu’un additionneur. Zéro n’a qu’une écriture, et \(-x\) s’obtient par \(\mathord{\sim}x + 1\).
Quand ça échoue
L’intervalle n’est pas symétrique : \(|T_{min}| = T_{max} + 1\). \(-T_{min}\) ne tient pas dans le type : sur un int, -INT_MIN est un débordement, donc un comportement indéfini.
static void ligne(const char *nom, int8_t x)
{
printf("%-5s %4d 0x%02x ", nom, x, (uint8_t)x);
for (int i = 7; i >= 0; i--)
putchar('0' + (((uint8_t)x >> i) & 1));
putchar('\n');
}
int main(void)
{
ligne("0", 0);
ligne("-1", -1);
ligne("T_max", 127);
ligne("T_min", -128);
ligne("-3", -3);
ligne("120", 120);
return 0;
}Résultat \(\rightarrow\)
0 0 0x00 00000000
-1 -1 0xff 11111111
T_max 127 0x7f 01111111
T_min -128 0x80 10000000
-3 -3 0xfd 11111101
120 120 0x78 01111000
Lecture d’un motif
\(-1\) : tous les bits à 1 (0xff) ; \(T_{max}\) : 0x7f ; \(T_{min}\) : 0x80. \(1111\,1101_2 = -128+125 = -3\) ; \(0111\,1000_2 = 120\).
Lecture. (uint8_t)x montre les bits de x sans changer le motif : c’est le même octet, lu sans signe. Sur 32 bits, un int va de \(-2^{31} = -2147483648\) à \(2^{31}-1 = 2147483647\).
Bits relevés en mémoire sous gdb ; ligne du haut : le poids de chaque bit.
Sortie \(\rightarrow\)
~x = 0xfa y = -5
~x + 1 = 0xfb x + y = 0
Lecture. x \(=5 = 0000\,0101_2\) ; sur un type signé, le bit 7 (orange) pèse \(-128\).
Lecture. ~x inverse chacun des bits : \(1111\,1010_2\) (0xfa).
Lecture. \(+1\) : le bit 0 passe de 0 à 1, aucune retenue ; 0xfb.
Lecture. Mêmes bits lus en signé : \(-128 + 64+32+16+8+2+1 = -5\) (conversion définie par l’implémentation, gcc garde les bits).
Lecture. Affichage de ~x.
Lecture. Affichage de ~x + 1.
Lecture. y vaut \(-5\) : \(-x = \mathord{\sim}x + 1\).
Lecture. x + y se calcule en int : \(5 + (-5) = 0\). Sur 8 bits, \(0000\,0101 + 1111\,1011\) donne aussi 0 : la retenue sort du 8e bit.
Huit octets relevés en 0x00 sur une machine little-endian (l’octet de poids faible d’un objet est à l’adresse la plus basse, Séance 3) :
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x00 |
7f7f |
8080 |
ffff |
00 |
fefefe |
010101 |
c8c8c8 |
3838 |
Énoncé
Donner (a) u[0] et s[0], (b) u[1] et s[1], (c) u[2] et s[2], (d) u[6] et s[6], (e) *h, (f) *n, en hexadécimal puis en décimal.
Correction
0x7f, bit 7 à 0 : 127 dans les deux lectures.0x80 : 128 sans signe, \(-128\) en complément à deux.0xff : 255, et \(-128+127 = \mathbf{-1}\).0xc8 : 200, et \(200 - 256 = \mathbf{-56}\).fe 01, poids faible d’abord : 0x01fe \(=\) 510.fe 01 c8 38 lus de +7 vers +4 : 0x38c801fe \(=\) 952631806, bit 31 à 0.Entre un type signé et le type non signé de même taille, la conversion garde les bits et change leur lecture.
Règles (C17 6.3.1.3)
Vers un non signé : valeur modulo \(2^w\), toujours définie : \(T2U_w(x) = x + 2^w\) si \(x < 0\), \(x\) sinon.
Vers un signé, si la valeur n’y tient pas : défini par l’implémentation ; gcc garde les bits : \(U2T_w(u) = u - 2^w\) si \(u > T_{max}\), \(u\) sinon.
Résultat \(\rightarrow\)
-3 -> 253
-1 -> 4294967295
3000000000 -> -1294967296
Lecture. 0xfd vaut 253 sans signe et \(-3\) en complément à deux ; \(-1\) devient \(-1 + 2^{32} = 4294967295\), et \(3000000000 - 2^{32} = -1294967296\).
Bits relevés sous gdb ; numéros de bit au-dessus (31 à gauche).
Sortie \(\rightarrow\)
si >= ui
v = 4294967295
Lecture. si \(=-1\) : en complément à deux, les 32 bits à 1 (0xffffffff).
Lecture. ui \(=0\) : 32 bits à 0.
Lecture. int contre unsigned int : si est converti en unsigned int, mêmes bits, valeur \(2^{32}-1\) ; \(4294967295 < 0\) est faux.
Lecture. La branche else s’exécute ; gcc l’annonçait (-Wsign-compare).
Lecture. Même conversion dans une affectation : les bits sont copiés tels quels.
Lecture. \(-1 + 2^{32} = 4294967295\) : la valeur que la Séance 1 obtenait pour (unsigned)-1.
Chaque ligne affiche une expression et sa valeur (VOIR(e) imprime #e, puis (int)(e)) :
Compilateur \(\rightarrow\)
s3_pieges.c:10:22: warning: comparison of integer expressions of different signedness: 'unsigned int' and 'int' [-Wsign-compare]
Résultat \(\rightarrow\)
-1 < 0U 0
-1 > -2 1
2147483647 > -2147483647 - 1 1
2147483647U > -2147483647 - 1 0
2147483647U > -2147483648 1
sizeof(-2147483648) 8
Règle
Si un opérande est unsigned int et l’autre int, l’int est converti en unsigned int (conversions arithmétiques usuelles, C17 6.3.1.8).
L’expression -2147483648
C’est - appliqué à 2147483648, qui ne tient pas dans un int : en C17, 2147483648 est un long (8 octets en LP64). La comparaison se fait alors en long et vaut 1. Pour \(T_{min}\), écrire -2147483647 - 1 ou INT_MIN.
Lecture. -1 < 0U : \(-1\) devient 4294967295, faux. 2147483647U > -2147483647 - 1 : \(T_{min}\) devient \(2^{31} = 2147483648\), faux. Sur une machine 32 bits en C90, -2147483648 était un unsigned long : le résultat dépend de la norme et du modèle.
Une conversion change aussi le nombre de bits : vers un type plus large, des bits s’ajoutent à gauche ; vers un type plus étroit, des bits disparaissent.
Extension, de \(w\) à \(w' > w\) bits
Non signé : des 0 à gauche ({zero extension}) ; 0xfa devient 0x00fa.
Signé : le bit de signe est recopié ({sign extension}) ; 0xfa (\(-6\)) devient 0xfffa, toujours \(-6\).
La valeur est conservée dans les deux cas.
Troncature, de \(w\) à \(w' < w\) bits
Seuls les \(w'\) bits de poids faible restent : valeur modulo \(2^{w'}\) vers un non signé (défini), et, vers un signé trop petit, résultat défini par l’implémentation (gcc : les mêmes bits lus en complément à deux).
Promotions
Dans une expression, char et short sont d’abord étendus en int (Séance 1) : chaque promotion est une extension.
Bits relevés sous gdb ; en orange, le bit de signe.
Sortie \(\rightarrow\)
s = -6 z = 250 r = 250
Lecture. c \(=-6\) : \(1111\,1010_2\) sur 8 bits (bit de signe à 1).
Lecture. Extension de signe : le bit de signe de c est recopié dans les 24 bits ajoutés ; s vaut toujours \(-6\).
Lecture. u \(=250\) : les mêmes 8 bits que c, lus sans signe.
Lecture. Extension par zéros : un type non signé reçoit des 0 à gauche ; z \(=250\).
Lecture. Le cast (uint8_t) passe d’abord c en non signé (250), puis l’extension ajoute des zéros : l’ordre décide.
Lecture. s : \(-6\).
Lecture. z : 250.
Lecture. r : 250, et non 0xfffffffa.
Bits relevés sous gdb ; hachurés : les bits de poids fort qui ne sont pas recopiés.
Sortie \(\rightarrow\)
t = -25536 c = -56
Lecture. big \(=40000 =\) 0x00009c40 : 32 bits, bit 31 à 0.
Lecture. Troncature : t garde les 16 bits de poids faible (0x9c40), les 16 autres sont perdus ; le bit 15, à 1, devient le bit de signe.
Lecture. k \(=200 =\) 0x000000c8.
Lecture. 8 bits gardés : 0xc8 \(= 1100\,1000_2\), bit de signe à 1.
Lecture. \(40000 - 2^{16} = -25536\) : le résultat de (short)40000 en Séance 1.
Lecture. \(200 - 2^{8} = -56\) : vers un type signé trop petit, la conversion est définie par l’implémentation (C17 6.3.1.3 §3) ; gcc garde les bits.
Énoncé. Prévoir les quatre lignes affichées, en nommant la conversion appliquée (machine du cours, LP64).
Correction
x converti en unsigned int : \(4294967295 < 0\) est faux, 0.0xf6 devient 0xfffffff6, lu sans signe : \(2^{32} - 10 =\) 4294967286.0x12345678 ne tient pas dans un signed char : conversion définie par l’implémentation ; gcc garde l’octet 0x78, bit 7 à 0 : 120.sizeof est un size_t (unsigned long) : x devient \(2^{64} - 1\) : -1 > 4.Un résultat qui ne tient pas sur \(w\) bits déborde ; la norme ne traite pas de la même façon les types non signés et signés.
Modulo \(2^w\) (C17 6.2.5 §9)
Un calcul sur des non signés ne déborde jamais au sens de la norme : le résultat est réduit modulo \(2^w\).
\[ \text{résultat} = (\text{valeur mathématique}) \bmod 2^w \]
Sur unsigned int : UINT_MAX + 1 \(= 0\), 0u - 1 \(= 4294967295\). Sur uint8_t, même résultat, mais par la conversion au retour : \(255+1 \to 0\), \(0-1 \to 255\), \(200+100 \to 44\).
Détection
Avant le calcul : a + b dépasse UINT_MAX si et seulement si b > UINT_MAX - a.
Après le calcul : une somme non signée plus petite qu’un de ses opérandes signale une retenue perdue.
Petits types
uint8_t et int8_t sont promus en int : le calcul se fait sur 32 bits, puis le résultat est converti au retour dans la variable.
Bits relevés sous gdb ; les retenues et le 9e bit sont calculés à partir de a et b.
Sortie \(\rightarrow\)
s = 44 retenue perdue
Lecture. a \(=200 = 1100\,1000_2\).
Lecture. b \(=100 = 0110\,0100_2\).
Lecture. Addition colonne par colonne, retenues en rouge ; \(300\) demande 9 bits : le 9e (poids 256) ne tient pas dans s, il est perdu.
Lecture. a + b se calcule en int (300, sans débordement) ; la conversion vers uint8_t réduit modulo 256 : 44, toujours défini (C17 6.3.1.3 §2).
Lecture. Une somme non signée plus petite qu’un opérande trahit la retenue perdue : \(44 < 200\).
Lecture. Test vrai : la retenue est détectée après coup, sans comportement indéfini.
Résultat \(\rightarrow\)
c = -128
y = -2147483648 = 0x80000000
C17 6.5 §5
Si le résultat d’une opération sur des signés n’est pas représentable dans son type, le comportement est indéfini : aucun résultat n’est garanti, et le compilateur peut supposer que cela n’arrive pas.
Sortie d’erreur avec -fsanitize=undefined \(\rightarrow\)
s5_signe.c:11:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
Lecture. c + 1 se calcule en int (128, sans débordement) ; c’est la conversion vers int8_t qui donne \(-128\), définie par l’implémentation. x + 1, en int, déborde : indéfini. Ici 0x7fffffff + 1 donne 0x80000000, le \(-2147483648\) de la Séance 1, et -fsanitize=undefined le signale à la ligne 11 ; en Séance 1, gcc avait déjà remplacé x + 1 < x par « faux ».
Résultat \(\rightarrow\)
deborde : 1
builtin : 1
Signé
a + b déborde si b > 0 && a > INT_MAX - b ou si b < 0 && a < INT_MIN - b : ces deux soustractions ne débordent jamais. gcc fournit aussi __builtin_add_overflow.
Failles réelles
CVE-2009-1385 : un {integer underflow} sur une longueur de trame, dans le pilote réseau e1000 de Linux, permettait de faire paniquer le noyau à distance.
Lecture. \(2\,000\,000\,000 > 2\,147\,483\,647 - 1\,000\,000\,000\) : la somme ne tiendrait pas dans un int, elle n’est pas calculée. __builtin_add_overflow range la somme réduite modulo \(2^{32}\) dans r et rend 1.
| type | opération | octet | valeur |
|---|---|---|---|
uint8_t |
\(240 + 20\) | 0x04 |
4 |
uint8_t |
\(250 + 10\) | 0x04 |
4 |
uint8_t |
\(5 - 9\) | 0xfc |
252 |
uint8_t |
\(100 - 150\) | 0xce |
206 |
int8_t |
\(127 + 1\) | 0x80 |
-128 |
int8_t |
\(-128 - 1\) | 0x7f |
127 |
int8_t |
\(100 + 20\) | 0x78 |
120 |
int8_t |
\(-100 - 50\) | 0x6a |
106 |
Énoncé
Pour chaque ligne, donner l’octet résultat et sa valeur, puis dire si le résultat est défini, défini par l’implémentation ou indéfini. Que vaut -x pour int x = INT_MIN ?
Correction
int : 260 deux fois ; conversion vers uint8_t, modulo 256 : 4, défini (C17 6.3.1.3 §2).int : 128 et \(-129\) ; conversion vers int8_t : \(128-256 = -128\), \(-129 + 256 = 127\), défini par l’implémentation.int : débordement, indéfini, aucune valeur à donner.Les opérateurs de la Séance 1 agissent colonne par colonne ; un masque choisit les bits sur lesquels agir.
| opérateur | bit résultat |
|---|---|
a & b |
1 si les deux bits valent 1 |
a | b |
1 si l’un au moins vaut 1 |
a ^ b |
1 si les deux bits diffèrent |
~a |
l’inverse de chaque bit |
a << k |
bits décalés de \(k\) vers les poids forts |
a >> k |
bits décalés de \(k\) vers les poids faibles |
Quatre gestes sur le bit \(k\)
Tester : (x >> k) & 1
Mettre à 1 : x | (1u << k)
Mettre à 0 : x & ~(1u << k)
Inverser : x ^ (1u << k)
Extraire \(n\) bits à partir de \(k\) : (x >> k) & ((1u << n) - 1)
Types non signés
Les masques s’écrivent en non signé (1u) : 1 << 31 est indéfini sur un int, et >> d’un négatif dépend de l’implémentation.
Bits relevés sous gdb ; la rangée « masque » montre le masque de la ligne exécutée.
Sortie \(\rightarrow\)
0 a a7 a1 25
Lecture. x \(=\) 0xa5 \(= 1010\,0101_2\).
Lecture. Tester : x >> 3 amène le bit 3 en position 0, & 1 garde ce seul bit : 0.
Lecture. Mettre à 1 : | avec un masque qui n’a que le bit 1 ; les autres bits sont recopiés.
Lecture. Mettre à 0 : & avec le complément du masque (tous les bits à 1 sauf le bit 2).
Lecture. Inverser : ^ avec le masque change le bit 7, et lui seul.
Lecture. Extraire un champ : x >> 4 amène les bits 7 à 4 en bas, & 0x0f efface le reste : 0xa.
Lecture. Le bit 3 testé et le champ extrait.
Lecture. x n’a jamais changé : chaque opération produit une nouvelle valeur.
Bits relevés sous gdb ; en rouge, les bits entrés par le décalage.
Sortie \(\rightarrow\)
-96 -6
ffffffa0
0ffffffa
80
Lecture. s \(=-96\) \(=\) 0xffffffa0.
Lecture. >> d’un négatif : défini par l’implémentation (C17 6.5.7 §5) ; gcc recopie le bit de signe (décalage arithmétique) : \(-96/16=-6\).
Lecture. Mêmes bits, type non signé : \(2^{32}-96\).
Lecture. >> d’un non signé : des 0 entrent à gauche (décalage logique) ; \(\lfloor 4294967200/16 \rfloor\).
Lecture. << 4 : des 0 entrent à droite, \(5 \times 2^4 = 80\) ; sur un signé, << est indéfini si le résultat ne tient pas (C17 6.5.7 §4).
Lecture. Le signe est conservé par >> sur int (gcc).
Lecture. u : les bits de s.
Lecture. Sur unsigned, >> divise par \(2^4\) sans tenir compte du bit 31.
Lecture. m affiché : 80. Un décalage de 32 positions ou plus, ou d’un nombre négatif, serait indéfini (C17 6.5.7 §3).
uint16_t r = 0x5a3c; : ses 16 bits, du bit 15 au bit 0 :
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
00 |
1 |
00 |
1 |
11 |
00 |
11 |
00 |
00 |
00 |
11 |
11 |
1 |
1 |
0 |
00 |
Énoncé
(r >> 4) & 0xf ;(r >> 13) & 1 ;r | 0x0001 ;r & ~0x0f00 ;r ^ 0x8000 ;Correction
3).5 \(= 0101_2\) pour les bits 15 à 12).0x5a3d.0x503c.0xda3c.Les entiers ne couvrent ni les très grands nombres (\(1{,}5\times10^{11}\) m), ni les très petits (\(9{,}11\times10^{-31}\) kg), ni les fractions. Une virgule fixe gaspille des bits : la partie entière ne sert à rien pour \(0{,}000001\), la partie fractionnaire pour \(10^{15}\) ; la virgule doit « flotter ».
Bits après la virgule
\[ b_m\dots b_0\,,\,b_{-1}\dots b_{-n} = \sum_{i=-n}^{m} b_i\, 2^i \]
À droite de la virgule, les poids sont \(\frac12\), \(\frac14\), \(\frac18\), …
\(101{,}11_2 = 4 + 1 + 0{,}5 + 0{,}25 = 5{,}75\). Un nombre n’a une écriture binaire finie que s’il est de la forme \(k/2^n\) : \(0{,}1\) n’en a pas (\(0{,}0\,0011\,0011\dots_2\)).
Lecture. %a affiche un double en hexadécimal : 0x1.7p+2 se lit \(1{,}0111_2 \times 2^2 = 101{,}11_2\). Le double le plus proche de 0,1 vaut \(0{,}1000000000000000055\dots\)
Quoi
\(V = (-1)^s \times M \times 2^E\). Un float ({binary32}) : 1 bit de signe \(s\), 8 bits d’exposant \(e\), 23 bits de fraction \(f\) ; \(E = e - 127\) (exposant biaisé), \(M = 1{,}f\) (le 1 de tête n’est pas stocké). Un double : 1, 11 et 52 bits, biais 1023.
Pourquoi
La précision devient relative : 24 bits significatifs (environ 7 chiffres décimaux) pour un float, 53 (environ 16) pour un double, quelle que soit la grandeur du nombre, de \(10^{-38}\) à \(10^{38}\) en float.
Quand ça échoue
La plupart des décimaux sont arrondis (0,1) ; un petit terme ajouté à un grand disparaît ; l’addition n’est pas associative ; un résultat trop grand devient \(\pm\infty\), une opération sans valeur donne NaN.
101 suivi de 20 zéros (le 1 de tête est implicite).0 |
10000010 |
10100000000000000000000 |
|---|---|---|
| \(s\) | \(e = 130\) | \(f\) (23 bits) |
Groupes de 4 : 0100 0001 0101 0000 … \(=\) 0x41500000. Vérification : \(1{,}101_2 \times 2^3 = 1{,}625 \times 8 = 13\).
Résultat \(\rightarrow\)
0x41500000 : 00 00 50 41
Lecture. show_bytes (Bryant et O’Hallaron, § 2.1) affiche les octets dans l’ordre des adresses : 00 00 50 41, l’octet de poids faible d’abord.
Adresses simplifiées ; sur la machine, f est en 0x7fffffffebe4, u 4 octets plus loin.
Sortie \(\rightarrow\)
u = 0xc15a0000
1 130 0x5a0000
E = 3
Lecture. Les 4 octets de f, adresse croissante : 00 00 5a c1 ; little-endian : le poids faible d’abord.
Lecture. memcpy recopie les 4 octets dans un entier : u \(=\) 0xc15a0000, lu de l’octet +3 vers l’octet +0.
Lecture. Signe : u >> 31, le bit 31 : 1, nombre négatif.
Lecture. Exposant biaisé : bits 30 à 23, (u >> 23) & 0xff \(= 1000\,0010_2 = 130\) ; \(E = 130-127 = 3\).
Lecture. Fraction : les 23 bits de poids faible, \(101101\) puis 17 zéros ; \(M = 1{,}101101_2 = 1{,}703125\).
Lecture. u affiché en hexadécimal : l’ordre des chiffres est celui des poids, pas celui de la mémoire.
Lecture. Les trois champs.
Lecture. \(V = (-1)^1 \times 1{,}703125 \times 2^3 = -13{,}625\).
| catégorie | \(e\) | \(f\) | valeur |
|---|---|---|---|
| zéro | 0 | 0 | \(\pm 0\) |
| dénormalisé | 0 | \(\ne 0\) | \((-1)^s\, 0{,}f \times 2^{-126}\) |
| normalisé | 1 à 254 | tout | \((-1)^s\, 1{,}f \times 2^{e-127}\) |
| infini | 255 | 0 | \(\pm\infty\) |
| NaN | 255 | \(\ne 0\) | {not a number} |
NaN
NaN n’est égal à rien, pas même à lui-même : x == x est faux. Tester avec isnan(x). \(+0\) et \(-0\) sont égaux pour ==, mais n’ont pas les mêmes bits.
Résultat \(\rightarrow\)
+0 00000000
-0 80000000
+inf 7f800000
-inf ff800000
0/0 ffc00000
FLT_MAX 7f7fffff 3.40282e+38
FLT_MIN 00800000 1.17549e-38
denorm 00000001 1.4013e-45
nan == nan : 0
isnan(nan) : 1
+0 == -0 : 1
Lecture. \(1/0\) donne \(+\infty\) (7f800000). \(0/0\) donne ici ffc00000 : un NaN dont le bit de signe vaut 1, celui que produit le processeur. Le plus petit dénormalisé vaut \(2^{-149} \approx 1{,}4 \times 10^{-45}\).
Machine little-endian ; float *f = (float *)0x00;
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x00 |
0000 |
0000 |
d0d0 |
c0c0 |
0000 |
0000 |
4848 |
c1c1 |
0x08 |
0000 |
0000 |
8080 |
7f7f |
0000 |
0000 |
c0c0 |
7f7f |
Énoncé
f[0] ;f[1] ;f[2] ;f[3].Correction
00 00 d0 c0 lus de +3 vers +0 : 0xc0d00000.0xc1480000 : \(e = 130\), \(M = 1{,}1001_2 = 1{,}5625\) : \(-12{,}5\).0x7f800000 : \(e = 255\), \(f = 0\) : \(+\infty\).0x7fc00000 : \(e = 255\), \(f \ne 0\) : NaN.
Adresses simplifiées ; un double par ligne ; sur la machine, a est en 0x7fffffffebe0.
Sortie \(\rightarrow\)
c = 0.30000000000000004
t = 0.29999999999999999
c == t : 0
Lecture. 0,1 n’a pas d’écriture binaire finie : a reçoit le double le plus proche (0x3fb999999999999a).
Lecture. 0,2 de même : même mantisse, exposant plus grand de 1 (b9 devient c9).
Lecture. La somme exacte des deux approximations est arrondie au double le plus proche : ...333334.
Lecture. Le double le plus proche de 0,3 est ...333333.
Lecture. 17 chiffres significatifs montrent l’écart : c est au-dessus de 0,3.
Lecture. t est juste au-dessous de 0,3.
Lecture. Les deux mots ne diffèrent que d’une unité sur l’octet de poids faible (34 contre 33 en +0), soit \(2^{-54} \approx 5{,}6\times10^{-17}\) : == rend 0.
float a = 0.1f + 0.2f;
float b = 0.3f;
printf("float : %d\n", a == b);
double c = 0.1 + 0.2;
printf("double : %d\n", c == 0.3);
float x = 3.14f;
float big = 1e10f;
printf("%f %f\n", (x + big) - big, x + (big - big));
float acc = 0.0f;
for (int i = 0; i < 1000000; i++)
acc += 0.1f;
printf("%.2f\n", acc);
printf("%d\n", fabs(c - 0.3) < 1e-9);Résultat \(\rightarrow\)
float : 1
double : 0
0.000000 3.140000
100958.34
1
Lecture. En float, 0.1f + 0.2f et 0.3f sont arrondis au même float : l’égalité est vraie, par chance ; en double, elle est fausse. 3.14f est absorbé par \(10^{10}\) (en float, deux voisins de \(10^{10}\) sont distants de 1024). Un million d’ajouts de 0.1f donnent 100958,34 au lieu de \(100\,000\). Comparer deux réels se fait avec une tolérance : fabs(c - 0.3) < 1e-9.
Résultat \(\rightarrow\)
erreur par tic : 9.537e-08 s
apres 100 h : 0.3433 s
distance : 575 m
Patriot, Dhahran, 25 février 1991
Dixièmes de seconde multipliés par 0,1, rangé avec 23 bits après la virgule (registre à virgule fixe de 24 bits). Après 100 h : dérive de 0,34 s, soit 575 m parcourus par le Scud ; fenêtre de poursuite décalée de 687 m (GAO). Scud non intercepté : 28 soldats tués.
Softmax en IA
\(e^{1000} = +\infty\), et \(\infty/\infty\) donne NaN (-nan : bit de signe à 1) ; soustraire d’abord le maximum (s8_softmax.c) :
naif : -nan
stable : 0.665241
Lecture. IA : float16 (1, 5, 10 bits), bfloat16 (1, 8, 7 bits), int8 quantifié.
Énoncé. Prévoir les six lignes affichées (quatre questions), et justifier chaque valeur.
float a = 0.7f + 0.1f;
float b = 0.8f;
printf("a == b ? %d\n", a == b);
double c = 0.7 + 0.1;
printf("c == 0.8 ? %d\n", c == 0.8);
float x = 1e30f * 1e10f;
printf("x = %f\n", x);
printf("isinf(x) = %d\n", isinf(x) != 0);
float y = x - x;
printf("y == y ? %d\n", y == y);
printf("isnan(y) = %d\n", isnan(y) != 0);Correction
float est arrondie sur 0.8f (0x1.99999ap-1).double, 0.79999999999999993 contre 0.80000000000000004.FLT_MAX : inf, isinf : 1.y == y : 0, isnan : 1.Résultat \(\rightarrow\)
a == b ? 1
c == 0.8 ? 0
x = inf
isinf(x) = 1
y == y ? 0
isnan(y) = 1
Types de taille fixe
<stdint.h> : int8_t … int64_t, uint8_t … uint64_t ; <inttypes.h> pour les afficher : printf("%" PRIu32 "\n", n).
Comparaisons mixtes
Tester le signe d’abord : i >= 0 && (unsigned)i < n ; ne pas mélanger int et size_t dans une condition.
Calculs
Vérifier avant une addition signée ; manipuler les bits sur des non signés ; comparer des réels avec une tolérance ; préférer double à float.
À retenir
int comparé à un unsigned devient unsigned.& teste ou efface, | met à 1, ^ inverse, >> et & extraient un champ.float ; le dump est little-endian ; 0,1 est arrondi, == entre réels est un piège.Séance 3 : organisation de la mémoire
Les octets de cette séance prennent une adresse : mot machine et endianness, pointeurs et indirection, tableaux et arithmétique des pointeurs, chaînes, pile et tas, malloc et free, et show_bytes pour afficher n’importe quel objet.
À préparer
Le TD et le TP de la séance 2 : conversions de base, dumps little-endian à lire, conversions et débordements à prévoir, masques, encodage IEEE 754.
show_bytes, opérations bit à bit), § 2.2 (entiers non signés, complément à deux, conversions, extension, troncature), § 2.3 (arithmétique, débordements), § 2.4 (IEEE 754, arrondi).Mesures de la séance : x86-64, Linux 6.18, gcc 13.3.0 (-std=c17 -Wall -Wextra -O0 -g), glibc 2.39, gdb 15.1 ; adresses rendues déterministes par setarch -R.
Programmation C avancée — Séance 2 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture.
q\(=186\), les huit cases debà 0,i\(=0\) :b[i]recevra le bit de poids \(2^i\).Lecture. Tour 1 : \(186 = 2 \times 93 + 0\) ; le reste 0 va dans
b[0](poids \(2^0\)),qdevient 93.Lecture. Tour 2 : \(93 = 2 \times 46 + 1\) ; le reste 1 va dans
b[1](poids \(2^1\)),qdevient 46.Lecture. Tour 3 : \(46 = 2 \times 23 + 0\) ; le reste 0 va dans
b[2](poids \(2^2\)),qdevient 23.Lecture. Tour 4 : \(23 = 2 \times 11 + 1\) ; le reste 1 va dans
b[3](poids \(2^3\)),qdevient 11.Lecture. Tour 5 : \(11 = 2 \times 5 + 1\) ; le reste 1 va dans
b[4](poids \(2^4\)),qdevient 5.Lecture. Tour 6 : \(5 = 2 \times 2 + 1\) ; le reste 1 va dans
b[5](poids \(2^5\)),qdevient 2.Lecture. Tour 7 : \(2 = 2 \times 1 + 0\) ; le reste 0 va dans
b[6](poids \(2^6\)),qdevient 1.Lecture. Tour 8 : \(1 = 2 \times 0 + 1\) ;
qvaut 0, la boucle s’arrête : le dernier reste est le bit de poids fort.Lecture.
kva de 7 à 0 : les restes s’affichent du dernier au premier, \(186 = 1011\,1010_2\).