- Dans la branche master de Zig, des améliorations du traitement des entiers non ABI dans le backend LLVM ainsi qu’une nouvelle sémantique de
@bitCastont été fusionnées, ce qui règle à la fois des problèmes d’optimisation et des incohérences de comportement du langage - Les entiers à largeur de bits arbitraire comme
u4,i13ouu40sont désormais traités comme des bit-int dans les valeurs SSA, mais étendus à des entiers de taille ABI lors de leur stockage en mémoire - L’ancien
@bitCastse rapprochait d’une réinterprétation des octets mémoire, mais la nouvelle définition s’appuie sur le tableau logique de bits du type, afin de réduire la dépendance à l’endianness - Le changement s’étend aux backends LLVM et C ainsi qu’à l’exécution
comptime, et les usages associés dans la bibliothèque standard, le compilateur etcompiler_rtont aussi été passés en revue - Avec le retour de certaines optimisations LLVM auparavant manquées, une amélioration d’environ 5 % des performances a été observée sur le compilateur Zig lui-même, et quelques gains de performances à l’exécution sont attendus dans la 0.17.0
Changement du traitement des entiers à largeur de bits arbitraire dans le backend LLVM
- Jusqu’ici, Zig abaissait directement les types entiers à largeur de bits arbitraire comme
u4,i13ouu40vers les types bit-int de LLVM IR, à savoiri4,i13eti40 - Cette approche imposait des contraintes inutiles à l’optimiseur à cause de la sémantique de représentation mémoire de LLVM, et comme Clang ne génère pas ce genre de LLVM IR, ces chemins internes de LLVM n’étaient pas suffisamment testés
- Au cours des dernières années, des cas concrets d’optimisations manquées et de miscompilation ont effectivement été observés
- La nouvelle approche conserve les types bit-int pour la manipulation des valeurs SSA, mais lors de l’écriture en mémoire, effectue une extension par zéros ou une extension de signe vers des types de taille ABI comme
i8,i16oui32 - Cet abaissement s’aligne sur la manière dont Clang abaisse le
_BitInt(N)du C, ce qui devrait emprunter un chemin mieux pris en charge dans LLVM
Les limites de l’ancien @bitCast
- Conceptuellement, l’ancien
@bitCastse rapprochait du comportement suivant- obtenir un pointeur vers la valeur de l’opérande
- convertir ce pointeur en pointeur vers le type de destination
- charger la valeur depuis ce pointeur
- Autrement dit, l’ancienne définition était plus proche d’une réinterprétation des octets en mémoire que de la structure logique du type
- Avec le temps, le comportement réel s’est écarté de cette définition, et sur la plupart des cibles, il restait permis d’appliquer
@bitCastde[3]u8versu24alors même que@sizeOf(u24)est supérieur à@sizeOf([3]u8) - Le backend LLVM implémentait une sémantique de
@bitCastinsuffisamment spécifiée, et lorsqu’on a changé la manière de stocker en mémoire les types entiers, des comportements illégaux et des crashs sont apparus dans la suite de tests du compilateur - Plutôt que d’ajouter dans le backend LLVM une logique imitant l’ancien comportement, il a été choisi d’implémenter de façon générale la nouvelle définition de
@bitCast
Nouvelle sémantique de @bitCast
- La nouvelle sémantique repose sur la proposition de langage #19755, soumise puis acceptée en 2024
- Elle était déjà implémentée dans le backend x86_64 auto-hébergé, et cette modification l’étend aux backends LLVM et C ainsi qu’à l’exécution
comptime - Le nouveau
@bitCastfonctionne non pas sur les octets mémoire, mais sur l’ordre logique des bits qui représente le typeu5est composé de 5 bits logiques, du bit de poids faible au bit de poids fort[2]u5est composé de 10 bits logiques, avec les 5 bits du premier élément suivis des 5 bits du second
- Pour des conversions simples entre entiers, comme passer de
u8ài8de même taille, les bits sont conservés tels quels et le bit de poids fort est interprété comme bit de signe - La sémantique de
@bitCastentre un type entier et unepacked structou unepacked unionest également conservée
Comportements qui changent pour les tableaux et vecteurs
- Le point où la nouvelle sémantique diverge de l’ancienne concerne les types agrégés comme les tableaux et vecteurs
- Par exemple, avec
@bitCastde[2]u8versu16, l’ancienne sémantique donnait un résultat différent selon l’endianness de la cible- sur une cible big-endian, le premier élément du tableau devenait les 8 bits de poids fort
- sur une cible little-endian, le premier élément du tableau devenait les 8 bits de poids faible
- La nouvelle sémantique ne considère que la représentation logique des bits, elle est donc indépendante de l’endianness, et sur toutes les cibles le premier élément du tableau devient les 8 bits de poids faible
- En règle générale, elle se rapproche davantage de l’ancien comportement sur les cibles little-endian
- Elle permet aussi des conversions atypiques, comme convertir
[2]u3en@Vector(3, u2)- les bits logiques du tableau sont concaténés, puis lus par groupes de 2 bits pour former les éléments du vecteur
- on peut aussi l’utiliser pour décomposer un entier en vecteur de bits individuels avec
@bitCastvers@Vector(n, u1)
Propositions associées et migration
- Au cours de ce travail, de petites propositions acceptées liées à
@bitCastont également été implémentées - Comme la nouvelle sémantique diffère de manière significative de l’ancienne, les usages de
@bitCastdans la bibliothèque standard, le compilateur et les bibliothèques de support commecompiler_rtont été examinés - La PR associée est codeberg.org/ziglang/zig/pulls/35711, et sa fusion dans master a aussi permis de fermer plusieurs issues
- La sémantique modifiée et la procédure de migration recommandée seront récapitulées dans les notes de version de Zig 0.17.0
Effets attendus sur les performances dans la 0.17.0
- Le changement de lowering des entiers non ABI dans le backend LLVM, qui était l’objectif initial, a bien permis de réactiver des optimisations manquées
- Le résultat associé peut être vérifié ici : demonstrably successful
- Bien que le compilateur Zig lui-même n’utilise pas massivement des entiers à largeur de bits arbitraire en interne, il affiche malgré tout un gain de performances d’environ 5 % grâce à une meilleure optimisation
- Dans la 0.17.0, de petits gains de performances à l’exécution pourraient apparaître sur certains codes
1 commentaires
Avis sur Lobste.rs
L’interprétation logique des bits évoquée dans l’article est présentée comme indépendante de l’endianess, mais l’explication réelle ressemble à une approche clairement little-endian qui ne prend pas en charge l’ordre des bits ou des octets en big-endian
Un nouveau journal de développement daté du 25 juin 2026 indique que la nouvelle sémantique de
@bitCastet des améliorations du backend LLVM ont été fusionnées dans une pull request récenteC’est intéressant, mais je me demande si, sur des cibles big-endian rarement testées, du code écrit comme ci-dessous ne risque pas soudainement de casser
En pseudo-code non-Zig :
En pratique, ça ne semble pas être un gros souci : parmi les milliers de
@bitCastdans le dépôt Zig, bien moins de 100 semblaient affectés par ce changementHonnêtement, je ne pense pas non plus que la plupart des utilisateurs de Zig savaient précisément comment
@bitCastse comportait entre tableaux/vecteurs et scalaires. Il y aura probablement aussi beaucoup de code qui n’était testé que sur la machine de son auteur et ne fonctionnait qu’en little-endian, mais qui fonctionnera désormais partoutEn tant qu’ancien programmeur C, je me souviens que les bit fields de C n’étaient pas très appréciés parce que leur comportement n’était pas portable d’une architecture à l’autre
La nouvelle sémantique de
@bitCastdans Zig me semble aller exactement dans la bonne direction : une sémantique abstraite portable qui donne le même résultat sur différentes architecturesJe conçois justement en ce moment des bit fields et des bit casts dans mon propre langage, donc je vais regarder de plus près les documents de conception et d’implémentation de Zig pour clarifier le comportement attendu dans mon code
packed structetpacked union, et les deux sont définis de manière cohérente avec la nouvelle définition de@bitCastUn
packed structremplit les bits des champs dans un « entier sous-jacent ». Par exemple, si les champs sontbool,u6,i9et que l’entier sous-jacent estu16, alors le bit de poids faible duu16correspond aubool, les 6 bits suivants auu6, et les 9 bits restants aui9. Autrement dit, les packed structs de Zig sont proches d’un sucre syntaxique au-dessus de plusieurs décalages et masquesUn
packed uniona lui aussi un entier sous-jacent, mais tous ses champs doivent utiliser exactement le même nombre de bits que cet entier. Écrire dans un champ puis lire dans un autre revient donc presque au@bitCastde la nouvelle sémantique. En revanche, les champs depacked union/packed structne peuvent pas avoir de type tableau ou vecteurPersonnellement, je trouve que ces outils conviennent bien pour exprimer des « structures liées aux bits ». On peut empaqueter plusieurs valeurs dans un
packed structpour l’utiliser comme les bit fields de C, et comme il s’agit essentiellement de sucre syntaxique au-dessus d’opérations sur les bits, on peut aussi représenter proprement des bit flags que l’on gérait en C avec des macros peu sûres du point de vue du typagePar exemple, des drapeaux d’accès RWX peuvent être exposés en C via des macros
ACCESS_READ,ACCESS_WRITE,ACCESS_EXECet une API prenant unuint8_t, alors qu’en Zig on peut définirAccess = packed struct(u8)avec des champsread,write,exec,reserved, puis faire accepterAccesspar l’APIpacked structetpacked unionpermettent aussi de représenter des dispositions de bits assez étranges. L’entrée de table des symboles du format objet Mach-O contient un champn_typeinhabituel, apparemment pour des raisons historiques ; on peut le modéliser avecbits: packed struct(u8)etstab: enum(u8)dans unpacked union(u8)Quand on manipule cette valeur
n_type, il n’y a pas besoin de faire des décalages ou des masquages à la main. Il suffit de vérifiern_type.bits.is_stab != 0et, si c’est vrai, de faire unswitchsurn_type.stab, sinon on regarde les autres champs den_type.bits. À l’inverse, on peut aussi construire des valeurs comme.{ .stab = .gsym }ou.{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } }Je me suis un peu étendu sur une autre fonctionnalité du langage que le sujet de l’article, mais si tu cherches quelque chose d’utile pour une nouvelle conception de langage, ça vaut la peine d’essayer directement
packed structetpacked uniondans Zig. Ce sont des outils simples, mais plutôt réussis à mon avis