Aller au contenu principal

Introduction à l'informatique · L1 · Section 3/7

Représentation de l’information

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Représentation de l'information

Toute information manipulée par un ordinateur (texte, nombre, image, son) finit en suites de bits. Comprendre cet encodage, c'est pouvoir prédire ses limites: pourquoi un accent casse, pourquoi un compteur reboucle, pourquoi un fichier grossit.

Prérequis: aucun. Quelques divisions euclidiennes suffisent.

Objectifs d'apprentissage:

  • Convertir un entier entre décimal, binaire et hexadécimal.
  • Compter les motifs qu'un type peut représenter (2k2^k sur kk bits) et en déduire ses bornes.
  • Expliquer l'encodage UTF-8 et le coût variable des caractères.
  • Prédire le comportement d'un entier borné (débordement) et d'un flottant (arrondi).
  • Justifier pourquoi aucune représentation exacte des réels n'est possible sur une machine.
  • Estimer la taille d'un texte ou d'une image à partir de sa représentation.

#Bases de numération

Un nombre s'écrit en base B avec des chiffres de 0 à B-1; chaque position vaut une puissance de B:

texttext

1Décimal (base 10):  13 = 1×10 + 32Binaire (base 2):   13 = 1×8 + 1×4 + 0×2 + 1×1 = 11013Hexadécimal (base 16): 13 = D  (un chiffre hexa vaut 4 bits)

L'hexadécimal sert de sténo pour le binaire: chaque chiffre hexa correspond exactement à 4 bits, donc un octet s'écrit avec deux chiffres hexa. 1101 1011 devient DB. C'est pourquoi les adresses mémoire, les couleurs et les empreintes s'affichent en hexa.

#Conversion par divisions euclidiennes

Pour convertir n en base B: diviser n par B, empiler les restes, lire de bas en haut.

texttext

113 en base 2:213 ÷ 2 = 6 reste 1   ↑3 6 ÷ 2 = 3 reste 0   ↑4 3 ÷ 2 = 1 reste 1   ↑5 1 ÷ 2 = 0 reste 1   ↑6Résultat lu de bas en haut: 1101

Vérification inverse: 1×8 + 1×4 + 0×2 + 1×1 = 13. Toute conversion se vérifie ainsi, dans les deux sens.

Animation:
n÷ basequotientreste
42÷ 2210 (0)
Résultat: 42 en base 10 = 101010 en base 2
Vitesse900ms

#Entiers bornés et débordement

Sur n bits non signés, on code les valeurs de 0 à 2^n - 1. Sur 8 bits: de 0 à 255. Ajouter 1 à 255 donne 0: la retenue sortante est perdue, la valeur « reboucle » (arithmétique modulo 256).

texttext

1   retenue : 1 1 1 1 1 1 1 12   a      : 1 1 1 1 1 1 1 1   (255)3   b      : 0 0 0 0 0 0 0 1   (1)4   ------------------------5   somme  : 0 0 0 0 0 0 0 0   (0, retenue perdue)

Python n'a pas ce problème sur les entiers (précision arbitraire), mais le C, le Java et la plupart des langages système si. Les conséquences sont réelles: un débordement de compteur peut fausser un instrument, faire échouer un système à une date donnée ou casser une clé de sécurité.

#Compter les mots binaires

Les bases de numération ne servent pas seulement à écrire des nombres: elles permettent de compter les représentations possibles, et donc de connaître la capacité exacte d'un type machine. Le principe est le même que celui d'un arrangement avec répétition: si l'on choisit un symbole parmi nn à chacune de pp positions, indépendamment, le nombre de mots obtenus est npn^{p}.

Sur l'alphabet binaire {0,1}\{0,1\} (n=2n = 2):

  • il y a 2p2^{p} mots de longueur exactement pp;
  • il y a 2p+112^{p+1} - 1 mots de longueur au plus pp (la somme 1+2+4++2p1 + 2 + 4 + \dots + 2^{p});
  • un octet, mot de 8 bits, admet donc 28=2562^{8} = 256 motifs distincts.

La longueur de la représentation binaire d'un entier n1n \ge 1 se déduit de ce comptage. Les mots de longueur kk couvrent les valeurs de 2k12^{k-1} à 2k12^{k}-1; le nombre de chiffres de nn est donc

bn=log2n+1.\lvert b_n \rvert = \lfloor \log_2 n \rfloor + 1.

Ainsi 13=1101213 = 1101_2 a log213+1=3+1=4\lfloor \log_2 13 \rfloor + 1 = 3 + 1 = 4 chiffres. Retenez la borne qui en découle: avec kk bits on ne peut nommer que 2k2^{k} objets distincts. C'est cette borne qui rend le principe des tiroirs si efficace pour prouver qu'un encodage est impossible (voir les annales corrigées).

#Capacité des types machine

Cette borne de 2k2^{k} explique les intervalles de valeurs des types des langages compilés. Un int Java est codé sur 4 octets, donc sur 32 bits, et dispose de 232=42949672962^{32} = 4\,294\,967\,296 motifs distincts — environ 4,3 milliards. Un long (8 octets, 64 bits) en dispose de 2641,8×10192^{64} \approx 1{,}8 \times 10^{19}.

Type JavaOctetsBitsMotifs distincts
byte18256
short21665 536
int4324 294 967 296
long86418 446 744 073 709 551 616
float4324 294 967 296
double86418 446 744 073 709 551 616

C'est cette capacité finie qui produit les deux pathologies vues plus haut. Pour les entiers, 2322^{32} motifs ne suffisent pas à contenir N\mathbb{N}: au-delà de la borne, la valeur reboucle. Pour les flottants, 2642^{64} motifs ne suffisent pas à contenir R\mathbb{R} — et cette fois aucune astuce d'implémentation ne peut y remédier, pour une raison mathématique que la section suivante expose.

#Flottants: des approximations

Un flottant stocke signe, mantisse et exposant en base 2; la plupart des décimaux n'y sont pas exacts:

pythonpython

1print(0.1 + 0.2)          # 0.300000000000000042print(0.1 + 0.2 == 0.3)   # False

Conséquence pratique: ne jamais comparer des flottants par égalité; comparer un écart à une tolérance (abs(a - b) < 1e-9), ou compter en entiers (centimes) quand l'exactitude importe.

#Pourquoi l'approximation est inévitable

On pourrait croire à un défaut d'implémentation, corrigible avec assez de bits. Il n'en est rien. Un double ne dispose que de 2642^{64} motifs, donc ne peut distinguer au plus que 2642^{64} nombres réels. Or l'intervalle [0,1][0,1] contient une infinité non dénombrable de réels: aucune application injective ne peut aller de R\mathbb{R} vers un ensemble de 2642^{64} motifs, par le principe des tiroirs. Toute représentation des réels sur une machine confond donc nécessairement deux réels distincts.

Autrement dit, un encodage exact des réels exigerait un nombre infini de bits — et un ordinateur ne manipule que des mots finis. C'est la raison de fond pour laquelle les flottants sont une convention d'approximation (IEEE 754), et non une représentation fidèle. La démonstration complète de la non-dénombrabilité de R\mathbb{R} est faite dans les annales corrigées.

#Encodage du texte: UTF-8

Un caractère est un point de code Unicode; UTF-8 l'encode sur un nombre variable d'octets (1 à 4) selon sa plage: ASCII sur 1 octet, accents européens sur 2, idéogrammes courants sur 3, émojis sur 4.

Chargement de l’éditeur...

#L'erreur classique: couper au milieu d'un caractère

pythonpython

1s = 'été'                  # 3 caractères2b = s.encode('utf-8')      # 5 octets: é(2) + t(1) + é(2)3print(list(b))             # [195, 169, 116, 195, 169]4try:5    print(b[:4].decode('utf-8'))   # on garde 4 octets sur 56except UnicodeDecodeError as e:7    print('décodage invalide:', e)

Les deux derniers octets (195, 169) forment le « é » final; n'en garder qu'un seul laisse une séquence incomplète et le décodage échoue. En revanche b[:3] donne b'\xc3\xa9t', qui se décode sans erreur en 'ét' — la coupe tombe alors exactement sur une frontière de caractère. C'est le bug des programmes qui tronquent des octets en croyant tronquer des caractères: la troncature n'échoue que si elle tombe au milieu d'une séquence, et un len exprimé en octets ne dit pas où sont ces frontières.

#Images et sons: même principe

Une image numérique est une grille de pixels; chaque pixel est un triplet (ou quadruplet) de composantes. Une image 1920×1080 en RGBA (4 octets par pixel) non compressée: 1920 × 1080 × 4 = 8 294 400 octets, environ 8 Mo. Les formats PNG et JPEG réduisent cette taille par compression, avec ou sans perte, mais la donnée de départ est cette grille. Le son suit la même logique: un échantillon toutes les 1/44100 seconde, 16 bits par échantillon, deux canaux.

#Codage: ce que le mot désigne exactement

Passer d'un objet à sa représentation binaire porte un nom précis. Un codage d'un alphabet AA dans un alphabet BB est une application injective de A+A^{+} (les mots non vides sur AA) dans B+B^{+}. L'injectivité est la condition non négociable: deux objets différents doivent recevoir deux codes différents, sans quoi le décodage serait ambigu.

Quelques codages usuels:

  • la représentation binaire d'un entier naturel, de l'ensemble des entiers vers les mots sur {0,1}\{0,1\};
  • le code Morse, de l'alphabet latin vers les mots sur {,.}\{-,.\};
  • le codage en base bb, qui associe à un mot sur un alphabet de lettres sa représentation dans {0,,b1}\{0,\dots,b-1\}.

Cette distinction entre « le code identifie l'objet » et « le code se relit tout seul » est la même que celle qui sépare l'injectivité de la bijectivité. Elle explique pourquoi UTF-8 a été conçu avec des octets de continuation reconnaissables (le préfixe 10): la synchronisation perdue au milieu d'un flux se retrouve, et une coupe accidentelle ne peut pas être confondue avec un caractère valide.

#Quiz éclair

Combien d'octets pour encoder 🙂 (U+1F642) en UTF-8 ?
Combien d'octets pour encoder 🙂 (U+1F642) en UTF-8 ?
Quelle opération provoque un débordement en entier non signé 8 bits ?
Quelle opération provoque un débordement en entier non signé 8 bits ?
Pourquoi 0.1 + 0.2 != 0.3 en Python ?
Pourquoi 0.1 + 0.2 != 0.3 en Python ?

#Exercices

  1. Conversion à la main. Convertissez 45 en binaire et en hexadécimal par divisions euclidiennes. Attendu: 101101 et 2D. Vérifiez avec bin(45) et hex(45).
  2. Taille d'un texte. Estimez le nombre d'octets de la chaîne 'café à côté' en UTF-8, puis vérifiez avec len(s.encode('utf-8')). Justifiez le décompte caractère par caractère.
  3. Compteur 8 bits. Écrivez un compteur 8 bits qui part de 250 et affiche les 8 valeurs suivantes en simulant l'arithmétique modulo 256. Attendu: 250..255 puis 0, 1.
  4. Choix de comparaison. Expliquez pourquoi total == 0.3 est un test fragile et écrivez la version robuste.
  5. Capacité d'un champ. Une base de données stocke un identifiant sur 12 bits. Combien d'objets distincts peut-elle nommer ? Que se passe-t-il à l'objet suivant ?
  6. Longueur binaire. Combien de chiffres binaires faut-il pour écrire 1000 ? Vérifiez avec la formule log2n+1\lfloor \log_2 n \rfloor + 1 puis avec len(bin(1000)) - 2.

#Corrections

Correction: conversion de 45
texttext

145 ÷ 2 = 22 r 1 ↑        45 ÷ 16 = 2 r 13 (D) ↑222 ÷ 2 = 11 r 0 ↑311 ÷ 2 =  5 r 1 ↑         2 ÷ 16 = 0 r 2 ↑4 5 ÷ 2 =  2 r 1 ↑5 2 ÷ 2 =  1 r 0 ↑         hexa: 2D6 1 ÷ 2 =  0 r 1 ↑7binaire: 101101

Vérification: 32 + 8 + 4 + 1 = 45; 2×16 + 13 = 45. En Python: bin(45) donne '0b101101', hex(45) donne '0x2d'.

Correction: taille d'un texte
pythonpython

1s = 'café à côté'2print(len(s))                    # 11 caractères3print(len(s.encode('utf-8')))    # 15 octets

Décompte: 'café à côté' compte 7 caractères ASCII (c, a, f, deux espaces, c, t) à 1 octet et 4 accentués (é, à, ô, é) à 2 octets: 7 × 1 + 4 × 2 = 15 octets, pour 11 caractères. C'est exactement l'écart entre len(s) et len(s.encode('utf-8')).

Correction: compteur 8 bits
pythonpython

1compteur = 2502for _ in range(8):3    print(compteur)4    compteur = (compteur + 1) % 2565# Sortie: 250 251 252 253 254 255 0 1

Le modulo 256 reproduit exactement le comportement matériel: la retenue sortante est équivalente au reste perdu.

Correction: comparaison robuste

total == 0.3 échoue dès que total est le résultat d'additions de flottants (0.1 + 0.2), car chaque opération décale la mantisse d'un ulp. Version robuste:

pythonpython

1assert abs(total - 0.3) < 1e-9

Ou en comptant en entiers (dixièmes): total_dixiemes == 3.

Correction: capacité d'un champ sur 12 bits

Sur k=12k = 12 bits, il y a 212=40962^{12} = 4096 motifs distincts: la base peut donc nommer 4096 objets, des identifiants 0 à 4095. Au 4097e objet, plus aucun motif n'est disponible; un compteur naïf reboucle alors à 0 et écrase l'identifiant du premier objet (arithmétique modulo 4096). C'est la version « base de données » du débordement d'entier, et la raison pour laquelle les identifiants sont presque toujours dimensionnés largement (32 ou 64 bits).

Correction: longueur binaire de 1000

10001000 est compris entre 29=5122^{9} = 512 et 210=10242^{10} = 1024, donc log21000=9\lfloor \log_2 1000 \rfloor = 9 et la longueur vaut 9+1=109 + 1 = 10 chiffres. En Python, bin(1000) donne '0b1111101000' : le préfixe 0b compte pour deux caractères, d'où len(bin(1000)) - 2 = 10. Vérification par la valeur: 11111010002=512+256+128+64+32+8=10001111101000_2 = 512 + 256 + 128 + 64 + 32 + 8 = 1000.