David Madore's WebLog: Le format %a des flottants, et autres crottes de ragondins

[Index of all entries / Index de toutes les entréesLatest entries / Dernières entréesXML (RSS 1.0) • Recent comments / Commentaires récents]

↓Entry #2330 [older| permalink|newer] / ↓Entrée #2330 [précédente| permalien|suivante] ↓

(mardi)

Le format %a des flottants, et autres crottes de ragondins

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.

↑Entry #2330 [older| permalink|newer] / ↑Entrée #2330 [précédente| permalien|suivante] ↑

[Index of all entries / Index de toutes les entréesLatest entries / Dernières entréesXML (RSS 1.0) • Recent comments / Commentaires récents]