1 points par GN⁺ 5 시간 전 | 1 commentaires | Partager sur WhatsApp
  • 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 @bitCast ont é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, i13 ou u40 sont 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 @bitCast se 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 et compiler_rt ont 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, i13 ou u40 vers les types bit-int de LLVM IR, à savoir i4, i13 et i40
  • 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, i16 ou i32
  • 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 @bitCast se 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 @bitCast de [3]u8 vers u24 alors même que @sizeOf(u24) est supérieur à @sizeOf([3]u8)
  • Le backend LLVM implémentait une sémantique de @bitCast insuffisamment 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 @bitCast fonctionne non pas sur les octets mémoire, mais sur l’ordre logique des bits qui représente le type
    • u5 est composé de 5 bits logiques, du bit de poids faible au bit de poids fort
    • [2]u5 est 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 à i8 de 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 @bitCast entre un type entier et une packed struct ou une packed union est é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 @bitCast de [2]u8 vers u16, 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]u3 en @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 @bitCast vers @Vector(n, u1)

Propositions associées et migration

  • Au cours de ce travail, de petites propositions acceptées liées à @bitCast ont également été implémentées
    • interdiction de @bitCast avec des vecteurs de pointeurs : #18936
    • autorisation de @bitCast pour les enum : une partie de #35602
  • Comme la nouvelle sémantique diffère de manière significative de l’ancienne, les usages de @bitCast dans la bibliothèque standard, le compilateur et les bibliothèques de support comme compiler_rt ont é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

 
GN⁺ 5 시간 전
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

    • Ici, indépendant de l’endianess semble vouloir dire que le comportement ne change pas entre architectures little-endian et big-endian
  • Un nouveau journal de développement daté du 25 juin 2026 indique que la nouvelle sémantique de @bitCast et des améliorations du backend LLVM ont été fusionnées dans une pull request récente

  • C’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 :

    if target_is_little_endian {  
        my_int = @bitCast(my_array);  
    } else {  
        my_int = @bitCast([my_array[1], my_array[0]]);  
    }  
    
    • J’ai eu la même pensée, mais au final, repousser un changement inévitable ne ferait qu’aggraver le problème
      En pratique, ça ne semble pas être un gros souci : parmi les milliers de @bitCast dans le dépôt Zig, bien moins de 100 semblaient affectés par ce changement
      Honnêtement, je ne pense pas non plus que la plupart des utilisateurs de Zig savaient précisément comment @bitCast se 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 partout
  • En 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 @bitCast dans Zig me semble aller exactement dans la bonne direction : une sémantique abstraite portable qui donne le même résultat sur différentes architectures
    Je 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

    • La principale alternative de Zig aux bit fields de C est sans doute packed struct et packed union, et les deux sont définis de manière cohérente avec la nouvelle définition de @bitCast
      Un packed struct remplit les bits des champs dans un « entier sous-jacent ». Par exemple, si les champs sont bool, u6, i9 et que l’entier sous-jacent est u16, alors le bit de poids faible du u16 correspond au bool, les 6 bits suivants au u6, et les 9 bits restants au i9. Autrement dit, les packed structs de Zig sont proches d’un sucre syntaxique au-dessus de plusieurs décalages et masques
      Un packed union a 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 @bitCast de la nouvelle sémantique. En revanche, les champs de packed union / packed struct ne peuvent pas avoir de type tableau ou vecteur
      Personnellement, je trouve que ces outils conviennent bien pour exprimer des « structures liées aux bits ». On peut empaqueter plusieurs valeurs dans un packed struct pour 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 typage
      Par exemple, des drapeaux d’accès RWX peuvent être exposés en C via des macros ACCESS_READ, ACCESS_WRITE, ACCESS_EXEC et une API prenant un uint8_t, alors qu’en Zig on peut définir Access = packed struct(u8) avec des champs read, write, exec, reserved, puis faire accepter Access par l’API
      packed struct et packed union permettent aussi de représenter des dispositions de bits assez étranges. L’entrée de table des symboles du format objet Mach-O contient un champ n_type inhabituel, apparemment pour des raisons historiques ; on peut le modéliser avec bits: packed struct(u8) et stab: enum(u8) dans un packed 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érifier n_type.bits.is_stab != 0 et, si c’est vrai, de faire un switch sur n_type.stab, sinon on regarde les autres champs de n_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 struct et packed union dans Zig. Ce sont des outils simples, mais plutôt réussis à mon avis