1 points par GN⁺ 2024-10-19 | 1 commentaires | Partager sur WhatsApp
  • P3477R0 est une proposition de modification du standard visant à ne plus laisser la taille d’un octet en C++ à la valeur dépendante de l’implémentation CHAR_BIT, mais à la fixer à exactement 8 bits
  • Les plateformes modernes convergent déjà en pratique vers des octets de 8 bits, et GCC, LLVM et MSVC utilisent aussi des valeurs par défaut ou des macros liées fixées à 8
  • POSIX exige CHAR_BIT == 8 depuis POSIX.1-2001, et l’adoption de la représentation des entiers en complément à deux dans C++20 et C23 va dans le même sens
  • La prise en charge des octets non 8 bits laisse de petites exceptions dans l’ensemble du langage, des bibliothèques et de la toolchain, et crée selon l’auteur une charge liée à des cas limites peu cohérents avec l’usage réel du C++ moderne
  • Des architectures atypiques comme le PDP-10 ou certains DSP existent, mais la question centrale est de savoir si les nouveaux standards C++ doivent continuer à conserver cette complexité pour elles

Objectif des changements de P3477R0

  • C++ reprend la macro CHAR_BIT du C, et cette valeur est actuellement une valeur dépendante de l’implémentation indiquant le nombre de bits dans un octet
  • P3477R0 propose de modifier le standard C++ pour exiger officiellement que l’octet fasse 8 bits
  • À l’époque des débuts de l’informatique, la souplesse permettant différentes tailles d’octet avait du sens, mais l’idée de fond est qu’aujourd’hui presque tout le matériel moderne converge vers l’octet de 8 bits

État actuel des compilateurs et des plateformes

  • Les principaux compilateurs traitent déjà l’octet de 8 bits comme la réalité par défaut
    • GCC utilise 8 comme valeur par défaut, et aucun target upstream ne modifie cette valeur
    • LLVM définit __CHAR_BIT__ à 8
    • MSVC définit CHAR_BIT à 8
  • Parmi les anciens cas pris en charge par GCC, dsp16xx a été supprimé en 2004 et 1750a en 2002
  • Des recherches sur le Web montrent quelques ports GCC externes où BITS_PER_UNIT n’est pas égal à 8, mais ils ne semblent pas pertinents pour le C++ moderne

Tendance côté POSIX et représentation des entiers

  • POSIX impose depuis POSIX.1-2001 les conditions suivantes
    • un octet de 8 bits exactement
    • CHAR_BIT à 8
    • SCHAR_MAX à 127, SCHAR_MIN à -128, UCHAR_MAX à 255
  • POSIX explique qu’à la suite de l’ajout de int8_t, il faut un char sur 8 bits et une arithmétique en complément à deux
  • Depuis P0907r4, C++20 ne prend en charge que le stockage en complément à deux, et C23 suit la même direction
  • Parmi les systèmes d’exploitation actuellement conformes à POSIX, sont cités AIX, HP-UX, INTEGRITY, macOS, OpenServer, UnixWare, VxWorks et vz/OS

Le coût laissé par les octets non 8 bits

  • Les logiciels conçus pour des octets de 8 bits et ceux destinés à des octets non 8 bits ne sont pas compatibles entre eux, et le code C/C++ pour octets non 8 bits ressemble en pratique à des dialectes incompatibles du C et du C++
  • La prise en charge d’architectures à octets non 8 bits laisse une petite complexité inutile dans plusieurs parties du langage et des bibliothèques
  • Les compilateurs et les toolchains doivent continuer à gérer des cas limites qui ne reflètent pas les usages modernes
  • Les nouveaux programmeurs peuvent facilement être déroutés par cette caractéristique exotique du C++
  • Certains programmeurs expérimentés finissent, selon l’auteur, par perdre du temps sur une “portabilité” visant des plateformes inexistantes

Architectures d’exception et compromis possibles

  • La proposition reconnaît elle aussi que des processeurs à octets non 8 bits existent encore
  • La question clé est de savoir si ces processeurs sont pertinents pour le C++ moderne, et si leurs utilisateurs adopteront réellement les nouvelles versions du C++
  • Un compromis consistant à exiger CHAR_BIT % 8 == 0 est également évoqué, mais il n’a de sens que si le comité décide de continuer à prendre en charge des DSP ou autres processeurs où CHAR_BIT n’est pas 8 mais un multiple de 8
  • Le PDP-10 fait partie de la discussion, mais il est précisé que le PDP-11 utilise des octets de 8 bits
  • Certains DSP traitent des mots de 24 ou 32 bits comme des “octets”, ce qui était défendable à une époque où la taille des mots variait fortement et où la notion d’octet n’était pas standardisée

Orientation des changements de formulation dans le standard

  • L’idée est de modifier la définition de l’octet dans intro.memory afin d’indiquer explicitement que l’octet, unité de stockage de base du modèle mémoire du C++, fait 8 bits
  • Dans climits, une modification de formulation est proposée pour fixer CHAR_BIT à 8
  • Dans cstdint, puisque l’octet ferait 8 bits, les types entiers à largeur fixée comme int8_t, uint8_t et les macros associées ne seraient plus optionnels
  • Parmi les types utilisant _N_, ceux dont N n’est ni 8, ni 16, ni 32, ni 64 resteraient optionnels
  • La modification inclut aussi la suppression de quatre clauses mandates liées à CHAR_BIT == 8 dans localization

Relation avec le standard C

  • La proposition examine si C++ doit continuer à se préoccuper des architectures à octets non 8 bits
  • Le comité C peut aboutir à une conclusion différente pour le langage C
  • Il serait idéal que les deux comités s’alignent, mais cette proposition laisse au groupe de liaison WG14 et SG22 le soin d’informer WG21

1 commentaires

 
GN⁺ 2024-10-19
Avis sur Hacker News
  • Dans la série de JF « Ne pourrait-on pas reconnaître que les vrais ordinateurs fonctionnent tous comme ça ? », il y avait déjà un épisode sur le fait que les entiers signés sont en complément à deux : "Signed Integers are Two’s Complement"

  • En 1986, quand j’étais stagiaire, j’ai écrit du code C sur un BBN C/70 qui utilisait des octets de 10 bits ; c’était une expérience atroce, et le simple fait qu’une telle machine ait existé relevait d’un accident cosmique, au sens négatif

    • J’ai déjà codé sur un DECSYSTEM-20, mais le compilateur C n’était pas officiellement pris en charge
      Il utilisait des mots de 36 bits et des octets de 7 bits, et quand on remplissait un mot avec des octets, il restait des bits. Comme on m’avait chargé de lire des bandes contenant des données binaires au format 8 bits, c’était le chaos
    • J’ai programmé le CPU de l’Intel Intellivision, une machine étrange utilisant des decl de 10 bits, et elle n’était pas assez puissante pour faire tourner du C
    • J’ai travaillé sur une machine avec des octets de 9 bits et des instructions de 81 bits, ainsi que sur une machine à octets de 6 bits, mais aucune des deux n’avait de compilateur C
    • Dans les FPGA actuels, l’arithmétique 10 bits n’est vraiment pas rare et s’utilise même dans des produits relativement modernes
      Mais du C en 10 bits, c’est une autre histoire
  • D a fait un grand pas en avant en décidant que : un octet fait 8 bits, short fait 16 bits, int 32 bits, long 64 bits, l’arithmétique est en complément à deux, et les flottants sont des flottants IEEE
    Cela a évité un temps énorme passé à essayer d’abstraire ces choses pour finir par se tromper, et des millions de personnes ont poussé un soupir de soulagement. Le jeu de caractères était aussi Unicode, pas EBCDIC ni RADIX-50

    • Zig fait encore mieux : les tailles sont explicites, comme u8/i8, u16/i16, u32/i32, u64/i64, et l’arithmétique se choisit aussi explicitement
      Le débordement de + est un comportement incorrect, donc il interrompt l’exécution en debug et en releasesafe ; +% est un wrapping en complément à deux, +| est une arithmétique saturée. @addWithOverflow() renvoie un tuple du type d’origine et de u1, et std.math.add() renvoie une erreur en cas de débordement. f16, f32, f64, f80, f128 sont aussi des types flottants IEEE de la longueur en bits correspondante. La longueur d’un octet n’a pas d’importance : sur une machine à octets de 12 bits, il suffit d’utiliser u12 et i12
    • Dire que D a fait un grand pas en avant est exagéré. Des noms de types à taille explicite comme u8, i32 sont largement meilleurs à tous points de vue
    • Si « un octet fait 8 bits », quelle est la taille d’un bit ?
    • De la part de M. Bright, l’auteur du langage D, ça fait un peu autocongratulation, non ? :)
    • Java aussi a bien fait les choses sur ce point. Il a mal géré unsigned, mais il a correctement standardisé le nombre de bits des types primitifs
      byte = 8 bits, short = 16, int = 32, long = 64, float = 32 bit IEEE, double = 64 bit IEEE
  • Il y a encore des gens qui doivent gérer des DSP : https://thephd.dev/conformance-should-mean-something-fputc-and-freestanding#we-cannot-program-on--vibes-
    Personnellement, je documente pour m’amuser une console fantasy 12 bits non implémentée, avec « 50 % de bits en plus par octet que la concurrence ! », et j’y ai aussi mis des inventions comme « UTF-12 »

    • J’essaie encore de déterminer quelles cibles restent pertinentes, et si elles visent le C++ moderne ou prévoient de le faire
      Cela fait des années que je pose la question sans obtenir de réponse positive ; le seul exemple mentionné était à peu près TI, donc j’ai ajouté l’information dans le brouillon mis à jour : https://isocpp.org/files/papers/D3477R1.html
    • À mon avis, il suffit de cibler C++23 ou inférieur. J’ai quelques SHARC, mais si le comité retirait la prise en charge de CHAR_BIT=32 dans une version comme C++30, je ne hurlerais pas au scandale
    • Le PDP-8 n’utilisait-il pas des octets de 12 bits ?
  • Je me demande si C++ peut vraiment supprimer ou simplifier quelque chose
    C’est une vraie question, et je n’ai pas suivi le sujet en détail. J’ai entendu dire que rand() était cassé et impossible à corriger, mais aux dernières nouvelles il n’était même pas encore prévu pour dépréciation. Cette proposition ressemble à un test du type : « peut-on aussi supprimer la prise en charge de solutions à des problèmes que littéralement personne ne rencontre ? »

    • Les entiers signés n’étaient pas forcément obligés d’être en complément à deux ; les trois représentations — signe-valeur absolue, complément à un et complément à deux — étaient toutes valides
      Le C et le C++ modernes ont abandonné cela et exigent le complément à deux. Ici, la distinction « as if » n’a pas vraiment d’importance en pratique, et on peut l’appliquer de la même manière à CHAR_BIT, donc il existe clairement un précédent pour ce genre de changement
    • Les trigraphes ont déjà été supprimés, et rand a aussi été rendu déprécié, avec des alternatives fournies
      Il y a aussi p2809 Trivial infinite loops are not Undefined Behavior, p1152 Deprecating volatile, p0907 Signed Integers are Two's Complement, p2723 Zero-initialize objects of automatic storage duration, p2186 Removing Garbage Collection Support. Donc oui, changer les choses est possible
    • L’API de GC de C++11 a été supprimée en C++23, ce qui se comprend dans la mesure où elle n’avait pas été conçue en tenant compte des besoins des principales variantes prenant en charge le GC, Unreal C++ et C++/CLI
      Les spécifications d’exceptions ont aussi été supprimées, même si certains voudraient les ressusciter pour les exceptions de type valeur. auto_ptr a également été supprimé à cause de sa conception cassée. Cela dit, côté simplification, ce n’est pas vraiment mieux, puisqu’il faut toujours connaître l’ancienne façon de faire
    • On dirait une satire de l’idée qu’il ne faut pas briser la perfection, mais accumuler toujours plus de perfection
      Du genre : il faudrait un nouveau symbole C++ qui désigne de façon fiable un octet de 8 bits sans casser la compatibilité. Par exemple unsigned byte8, signed byte8 en complément à deux, et même byte8 avec un comportement de signe non défini. On pourrait aussi créer unsigned decimal byte8 et signed decimal byte8, limités aux plages 0~10 et -10~+10 pour les comptables, centimal byte8 de 0~100 et -100~+100 pour les comptables qui comptent même le coût des octets, un type vaguement suffisant pour le champ age d’une base de données, et bien sûr ajouter aussi float byte8
    • Je ne vois pas en quoi rand() est cassé. Il produit des valeurs qui ont l’air aléatoires, et c’est son but
      Évidemment qu’il ne produit pas de nombres aléatoires cryptographiquement sûrs, et les fonctions équivalentes dans d’autres langages non plus. Pour un entier assez aléatoire et rapide à calculer, rand() fonctionne suffisamment bien
  • Merci de vous intéresser à cette proposition ; j’ai préparé un brouillon mis à jour à partir des retours reçus jusqu’ici : https://isocpp.org/files/papers/D3477R1.html

    • J’aime bien le ton sarcastique de la proposition
      La phrase suivante m’a particulièrement marqué : « Le problème n’est pas de savoir s’il existe encore des architectures où un byte ne fait pas 8 bits. Il y en a ! Le problème est de savoir si elles se soucient du C++ moderne, et si le C++ moderne se soucie d’elles »
  • J’ai des sentiments partagés sur cette proposition. D’un côté, elle est manifestement correcte, et il n’existe pas d’usage significatif où CHAR_BIT ne vaut pas 8
    De l’autre, j’ai aussi l’impression qu’on cède à une vision du monde juste selon laquelle le monde devrait avoir du sens et être raisonnable à partir d’un modèle personnel et excessivement simplifié de l’intérieur des ordinateurs. Cette approche vous emmène assez loin, mais finit dans une impasse ; au bout du compte, il faut admettre qu’on ne sait rien et que le mieux qu’on puisse faire est un raisonnement formel disant qu’on a construit un programme correct à condition que la documentation soit correcte. C’est un grand saut intellectuel, et personnellement, plus on tarde à être forcé de l’admettre, plus il est difficile de le franchir ensuite. Cela dit, les projets d’électronique physique semblent redevenir populaires chez les débutants ces temps-ci, donc j’espère que « lis la documentation » sera remplacé comme nouveau standard par « lis cette foutue datasheet »

    • Et pourtant, chaque fois que je lance un script autoconf, je le vois vérifier le nombre de bits dans un byte et l’enregistrer dans config.h. Comme si quelqu’un avait vraiment prévu d’agir en fonction de cette valeur
    • Pour un langage largement utilisé, on finit forcément par se heurter au problème COBOL. Dans la plupart des cas ce sera acceptable, mais sur un système où la mise à jour est imposée, un système de contrôle du trafic pourrait soudainement s’arrêter, ou un avion s’écraser
      Il faudrait un moyen d’examiner tout l’ancien code pendant la compilation pour vérifier si cette macro est déjà utilisée. Ce type de changement cassant risque aussi de fracturer le langage. Il n’est pas évident non plus de tester si une base de code existante utilise la macro CHAR_BIT, ni si elle peut être mise à jour vers un nouveau compilateur. On peut aussi se demander quelles bibliothèques seraient considérées comme cassées, ou si des problèmes apparaîtraient lors de l’interaction avec d’autres codes compilés en utilisant CHAR_BIT. Je suis d’accord sur le fait que c’est contre-intuitif, mais il vaudrait mieux commencer par créer des outils de conversion, démontrer que c’est sûr même dans les cas extrêmes, puis seulement effectuer la transition
  • J’aime bien cette proposition : elle est à la fois incontestable et incroyablement épicée

  • Imposer int8_t == char == 8 bits, ça me va tout à fait, mais je ne suis pas sûr qu’il faille propager l’idée fausse selon laquelle un byte fait 8 bits
    Un byte de 8 bits s’appelle un octet. En même temps, depuis C++17, byte est déjà en quelque sorte un « alias » de char : https://en.cppreference.com/w/cpp/types/byte

    • J’ai commencé l’informatique il y a 45 ans, et à l’époque déjà, « byte » était défini comme une quantité de 8 bits
      En 45 ans, je n’ai jamais vu « byte » utilisé avec un autre sens ; donc s’il existe une définition de « byte » qui ne fait pas 8 bits, il faudrait une source
    • Les RFC réseau ont toujours utilisé le terme octet depuis le début
    • Non, un byte fait 8 bits
      Ce n’est pas un énoncé descriptif, c’est un énoncé normatif
    • Personnellement, je n’aime pas que int8 == signed char
      std::cout << (int8_t)32 << std::endl; devrait évidemment afficher 32
  • Cela n’a rien à voir avec C++, mais j’aime assez l’idée d’un micro-ordinateur rétro à octets de 6 bits. Le mot ferait alors 24 bits.
    Les micro-ordinateurs manipulent généralement un petit nombre d’objets et privilégient les tableaux plutôt que les pointeurs, ce qui permettrait d’économiser de la mémoire. Le VGA utilisait 6 bits par couleur, on peut créer un alphabet lisible avec une matrice de bits 6x4, les langages LISP de base ou Forth peuvent aussi tenir dans un alphabet 6 bits, et le System/360 d’origine n’utilisait que des adresses sur 24 bits. Une mémoire de 12 Mio composée d’unités de 6 bits adressables indépendamment devrait suffire à tout le monde. Si ce n’est pas assez, il suffit d’étendre naturellement FAT-12 en FAT-24, ou d’utiliser des pointeurs 48 bits, aussi utiles que des pointeurs 64 bits.

    • Ou alors, on peut utiliser des octets de 8 bits et des mots de 3 octets. Ça fait toujours 24 bits.