Comme le maître
zen De-Monyo l'a dit avant moi : le chemin de l'enfer est pavé
de petites crottes de ragondin
. C'est certainement vrai, au
moins, en informatique.
Pour donner un exemple de la manière dont les ragondins arrivent à
déposer des petits cadeaux malodorants sur les sentiers de roses que
les informaticiens ont arrangés avec amour, je voudrais donner
l'exemple du format %a
de printf()
. Ou
plutôt, de sa (non) disponibilité.
Comme chacun sait, les ordinateurs calculent en binaire. Ceci est
vrai non seulement pour les entiers mais aussi pour les nombres en
virgule flottante. Par exemple, si je calcule sqrt(2.)
en C sur une implémentation raisonnable utilisant les
flottants double
précision IEEE 754, le résultat va être stocké sous
la forme suivante sur 64 bits :
0011 1111 1111 0110 1010 0000 1001 1110 0110 0110 0111 1111 0011 1011 1100 1101
Le premier bit (que j'ai écrit en bleu) signifie que le nombre est positif ; les 11 suivants (en vert) représentent la valeur 1023 et indiquent que le nombre a pour exposant 1023 − 1023 = 0 (on décale l'exposant en soustrayant 1023), c'est-à-dire que la valeur comprise entre 1 et 2 définie par les autres bits va être multipliée par 20 = 1 (et le résultat sera donc entre 1 et 2) ; enfin, les 52 bits restants désignent le nombre binaire 1.01101010… (obtenu en ajoutant un 1 devant), c'est-à-dire 1 + 0×2−1 + 1×2−2 + 1×2−3 + 0×2−4 + ⋯. Le nombre ainsi représenté a une valeur décimale exacte : à savoir 1.4142135623730951454746218587388284504413604736328125 (ce n'est pas la racine carrée de 2, bien sûr, qui ne peut pas s'exprimer exactement avec un nombre fini de chiffres ni en binaire ni en décimal, et qui vaut environ 1.4142135623730950488…, soit à peu près 9.67×10−17 en moins), il s'agit du nombre flottant double précision le plus proche de la racine carrée de 2. Le problème est de savoir comment écrire (textuellement) un tel nombre : on peut en donner une représentation décimale exacte (je viens de le faire), mais c'est long et malcommode. Le minimum de chiffres décimaux qu'il faut donner pour retrouver le nombre ci-dessus par arrondi au flottant double précision le plus proche est 16 après la virgule (soit 1.4142135623730951) : si on tronque un chiffre avant, on n'obtient pas le nombre ci-dessus — ce qui est un peu agaçant, parce que le dernier chiffre décimal en question n'est pas le bon arrondi pour la racine carrée de 2 à cette précision décimale (ce serait 1.4142135623730950, seulement, il s'arrondi à un flottant double précision juste epsilon plus petit). Tout cela est assez lourdingue. Mais ce n'est pas de ce genre de problèmes mathématiques que je veux parler. Je veux juste souligner qu'utiliser des écrites décimales pour représenter des flottants informatiques, c'est soit casse-pied soit casse-gueule (d'un autre côté, l'écriture binaire est vraiment pénible à décoder).
Heureusement, il y a une solution intermédiaire qui est un bon
compromis : au lieu d'écrire le flottant en binaire ou en décimal, on
peut l'écrire en hexadécimal ; plus exactement, la convention standard
est d'écrire sa mantisse en hexadécimal (pour l'avoir de façon
exacte), et l'exposant en décimal (pour que ce soit plus lisible pour
un humain). On écrira : 0x1.6a09e667f3bcdp+0
, ce qui
doit se lire comme le nombre hexadécimal 1.6a09e667f3bcd
(soit 1 + 6×16−1 + 10×16−2 +
0×16−3 + ⋯) multiplié par 20=1
(le p
est une façon de marquer ce format d'écriture, un
peu comme le e
signale l'écriture décimale avec une
puissance de 10). Cette écriture désigne exactement le
flottant signalé ci-dessus (le double précision le plus proche de √2),
et il n'est pas très loin de l'écriture binaire au sens où si on
convertit l'hexadécimal 6a09e667f3bcd
en binaire, on
retrouve les chiffres de la mantisse (les chiffres en rouge ci-dessus,
un chiffre hexadécimal pour chaque bloc de quatre).
Cette façon d'écrire les flottants s'obtient, en C, avec le
format %a
de la fonction printf()
(ou, en
lecture, de scanf()
). Le format en question n'est pas
terriblement lisible par un humain, mais il l'est quand même nettement
plus que le binaire, et il a l'avantage immense de représenter de
façon exacte et inambiguë chaque flottant sans gâchis inutile, sans
complication démesurée, et en permettant de visualiser rapidement quel
est le dernier bit de précision. Si on veut stocker des flottants
dans un fichier texte pour les relire ensuite et s'assurer qu'on
a exactement les mêmes nombres, %a
est
indiscutablement la bonne façon de procéder (notons qu'on peut aussi
les utiliser dans un source C, par exemple écrire const double
lemniscate_length = 0x1.4f9f94f9f50b0p+2
; notons qu'on a aussi
le droit de placer la virgule différemment, par
exemple 0x5.3e7e53e7d42c1p+0
pour la même quantité, mais
ici les deux derniers bits du dernier chiffre hexadécimal seront
perdus en flottant double-précision, puisqu'il n'y a que 52 bits de
précision dans la mantisse : avec la convention de caler le dernier
bit significatif en fin de chiffre hexadécimal, les flottants double
précision « normaux » s'écrivent toujours 0x1.
suivis de
quelque chose).
Bon, alors trève de digression, si ce format %a
est
bel et bon, où sont les petites crottes de ragondin ?
Le problème est que ce format devrait être universellement
accepté par tous les programmes, toutes les bibliothèques, et
tous les langages de programmation, susceptibles d'entrer ou de sortir
des flottants. Or ce n'est pas le cas. Pourtant, ça fait plus de
quinze ans maintenant que ce format a été normalisé par la norme C99
du C — et je pense qu'il n'était pas totalement nouveau même en
1999. QUINZE ANS ! Et en tout ce temps, beaucoup des
programmes, des bibliothèques, et des langages de programmation qui
pourtant imitent largement le C en général, et souvent les formats
de printf()
en particulier, n'ont apparemment toujours
pas reçu le memento. Ou alors ils se sont contentés du service
minimal : avoir une fonction float_from_hex
ou float_to_hex
cachée dans une obscure bibliothèque
n'est pas une excuse valable : dans tout contexte où on a le droit
d'écrire 1.729e3
comme flottant, on devrait aussi avoir
le droit d'écrire 0x1.b04p10
pour la même valeur, et
toute fonction qui permet de produire une sortie devrait permettre de
produire l'autre de façon à peu près aussi commode.
Je ne vais pas dresser la liste (déprimante) des situations où ça
ne marche pas : je vais plutôt encourager mes lecteurs à essayer tous
leurs programmes ou langages préférés et constater par eux-mêmes
lesquels acceptent le nombre 0x1.b04p10
comme un nombre
valable partout où ils acceptent 1.729e3
. Vous pouvez
signaler les résultats dans les commentaires de cette entrée, mais
encore plus productif serait de soumettre un rapport de bug contre
chacun des programmes qui ne comprend pas parfaitement ce format,
surtout si vous pensez arriver mieux que moi à éviter de dire des
choses désagréablement sarcastiques sur le fait que l'implémentation
ne soit pas encore faite.
[Correction/précision : c'est
bien 0x1.b04p10
qui doit être reconnu
comme 1.729e3
, j'avais initialement oublié le
préfixe 0x
ci-dessus. Remarquons, ça peut aussi être
intéressant de vérifier que le programme ou langage
reconnaît 0x6c1
pour 1729 : si ce n'est pas le cas, à la
limite, il a une excuse (un autre format pour désigner l'hexadécimal,
pourquoi pas, mais quel qu'il soit, il ne faut pas se contenter des
entiers).]
Mais le plus probable, et c'est là la source majeure de crottes de
ragondin en informatique, est que les mainteneurs des différents
programmes ou langages à qui on fera le reproche de ne pas supporter
le format en question, sortiront toutes sortes d'excuses de la plus
pure mauvaise foi pour expliquer ce manque. Parmi les excuses
prévisibles : le fait que la grammaire du langage serait compliquée à
changer, le fait qu'il faille passer par un processus de
standardisation[#] avant d'y
toucher (avec souvent le cycle vicieux évident : on ne peut
standardiser que des ajouts qui auront été bien testés avant, et on ne
peut ajouter que des choses standardisées), le fait que la fonction
est disponible sous le nom d'une fonction float_from_hex
ou float_to_hex
cachée dans une bibliothèque obscure, ou
derrière une option -DENABLE_OBSCURE_LANGUAGE_FEATURES
ou
quelque chose comme ça[#], ou
encore le fait que ce langage est fait pour les débutants qui n'ont
pas besoin de manipulation fine de flottants ou simplement que ce
n'est pas le C donc il n'y a pas de raison de recopier les
fonctionnalités de ce langage. Tout ceci est, bien sûr, de la plus
pure hypocrisie, parce que la vraie raison est plus proche
du not
invented here
que d'autre chose.
[#] Oui, gcc
/glibc
, vous
êtes visés, vous qui exigez qu'on écrive quelque chose
comme -D_XOPEN_SOURCE=1
ou -D_DEFAULT_SOURCE=1
ou -std=gnu99
juste
pour pouvoir utiliser M_PI
dans un programme C99 (ou
C11). Franchement, ceci est d'une connerie invraisemblable : d'abord
et surtout, le standard C mérite une paire de baffes pour avoir défini
les fonctions trigonométriques dans <math.h>
sans
avoir défini π à la précision voulue (et bien sûr, maintenant c'est
trop tard pour l'ajouter rétroactivement, il faut attendre un nouveau
standard) ; et ensuite, gcc
est un peu pénible dans son
interprétation psychorigide du standard (franchement, croit-on une
seule seconde à l'existence d'un programme qui utiliserait
l'identificateur M_PI
autrement que pour le nombre π ?
si oui, de toute façon, le programmeur qui a pondu ça mérite d'être
pendu haut et court, pas que le compilateur ménage son programme).
C'est quelque chose de profondément déprimant en informatique : même quand on a trouvé la solution d'un problème ou d'un bug (le problème étant, ici, de représenter les flottants de façon fiable et reproductible, facilement corrélable à leur écriture binaire), il faut souvent se battre contre un nombre invraisemblable de moulins à vent pour que cette solution arrive vraiment à l'endroit où elle est censée arriver et puisse enfin servir. Typiquement, il faudra d'abord se battre pour convaincre les mainteneurs de tel ou tel programme que le problème est réel et que la solution est utile : même si on y arrive, la solution en question atterrira généralement dans une branche « développement » du programme qu'on prétend réparer, et il faudra attendre de nombreux mois, voire des années, pour que cette branche de developpement devienne la branche stable (apportant avec elle toutes sortes de nouveaux problèmes qu'on aura le plaisir à combattre de nouveau), et même une fois que c'est fait, il peut encore falloir très longtemps pour que le programme arrive vraiment sur les ordinateurs où on veut l'utiliser (par exemple parce que derrière les mainteneurs du programme, il y a encore les mainteneurs de la distribution : si on parle de Debian, quand on rate la fenêtre pour une distribution stable, on gagne un bon nombre d'années d'attente supplémentaire ; si on parle d'un téléphone mobile, il faudra souvent attendre le bon vouloir du fabricant, qui ne viendra sans doute jamais sauf problème de sécurité urgent et encore). Et s'il y a un processus quelconque de standardisation dans l'histoire, le nombre de gens à convaincre et d'années à attendre pendant que l'histoire passe de comité en comité, devient carrément colossal. On se noie dans les crottes de ragondin.
Et c'est particulièrement pénible quand on parle de fonctionnalités
transverses (i.e., qui devraient être transverses) à toutes
sortes de langages ou de contextes : le format %a
des
flottants, il devrait au moins être disponible partout où un langage
ou un programme prétend réutiliser la syntaxe du printf()
ou scanf()
du C, ce qui fait beaucoup d'endroits. Rien
de plus insupportable que les endroits où presque tout est
disponible mais pas absolument tout (dans le genre, il y a
aussi les pénibles qui ne comprennent pas le format %#x
de printf()
ou le format %e
ou %z
de strftime()
). Ce n'est pas comme si
c'était difficile de se tenir au courant des nouveautés
de printf()
pour les implémenter immédiatement, il n'y en
a pas toutes les semaines.
Dans le même ordre d'idées, je peux donner une expérience
personnelle précise : la famille de hachés
cryptographiques SHA-2
(c'est-à-dire SHA-224, SHA-256, SHA-384
et SHA-512) a été standardisée en 2001. Peu après,
j'ai commencé à râler que les fonctions en question n'étaient pas
encore disponibles sous forme d'utilitaires (sha256sum
,
etc.) sur les systèmes Unix habituels. En 2005, comme mes râleries ne
marchaient décidément pas, j'ai écrit le code d'utilitaires en
question et je l'ai soumis au projet GNU pour
inclusion dans les coreutils
. Sauf que, bien sûr,
les coreutils
avait subi un changement majeur entre
temps, donc j'ai dû réécrire mon code pour la version de
développement. Qui a mis je ne sais combien de mois ou d'années à
être distribuée. Entre temps, j'ai aussi dû signer un transfert de
copyright du code à la FSF (pour l'anecdote, j'ai
d'ailleurs reçu un autocollant en paiement, une sorte d'astuce légale
pour que le transfert de copyright soit effectif dans certaines
juridictions où il faut qu'il y ait une forme de rémunération). Puis
il a fallu encore du temps pour que ce code arrive vraiment sur les
machines que j'utilisais : entre temps je devais, à chaque mise à jour
des coreutils
pour un problème de sécurité quelconque,
refaire mon patch et le distribuer sur chacune des N
machines où je l'utilisais. Et ceci ne concerne que le
projet GNU : certains ont été encore plus lents à
avoir les utilitaires en question, il me semble que mon téléphone sous
Android/CyanogenMod ne les a
(via BusyBox) que
depuis très récemment, et je suis sûr qu'il existe encore des systèmes
qui ont un utilitaire pour calculer un SHA-1 mais
pas de SHA-2.
Ce que je n'arrive pas à comprendre, c'est pourquoi certaines fonctionnalités basiques peuvent prendre si longtemps à traverser le pipeline entre l'écriture et la disponibilité universelle, alors que par ailleurs certains programmes ou langages, et parfois les mêmes qui mettent si longtemps à incorporer ces fonctions si simples, nous inondent de changements profonds et incompatibles. Il faudrait décidément apprendre la litière aux ragondins.