1 points par GN⁺ 2025-06-08 | 1 commentaires | Partager sur WhatsApp
  • L’optimisation bas niveau peut être mise en œuvre facilement avec le langage Zig
  • Le compilateur effectue bien les optimisations dans la plupart des cas, mais il faut parfois exprimer plus clairement l’intention du programmeur pour obtenir de meilleures performances
  • Zig prend en charge la génération de code hautes performances et un métaprogrammation puissante grâce à l’exécution à la compilation (comptime)
  • Par rapport à Rust, Zig permet des optimisations plus fines grâce aux annotations et à une structure de code explicite
  • Pour des opérations répétitives comme la comparaison de chaînes, comptime peut générer un code assembleur supérieur à celui d’une fonction ordinaire

Optimisation et Zig

Comme le rappelle un avertissement bien connu, « tout est possible, mais ce qui est intéressant ne s’obtient pas facilement » : l’optimisation des programmes reste toujours une préoccupation majeure des développeurs. Qu’il s’agisse de réduire le coût de l’infrastructure cloud, d’améliorer la latence ou de simplifier un système, l’optimisation du code est indispensable. Cet article explique surtout les concepts d’optimisation bas niveau dans Zig ainsi que les points forts du langage.

Peut-on faire confiance au compilateur ?

  • On entend souvent le conseil « faites confiance au compilateur », mais dans la pratique il arrive que le compilateur se comporte différemment des attentes ou viole les spécifications du langage
  • Les langages de haut niveau rendent plus difficile l’expression claire de l’intention et imposent donc certaines limites en matière de performances
  • Les langages de bas niveau, grâce au caractère explicite du code, permettent au compilateur de connaître les informations nécessaires à l’optimisation ; par exemple, si l’on compare une fonction maxArray en JavaScript et en Zig, Zig transmet dès la compilation — et non à l’exécution — des informations claires sur les types, l’alignement ou encore la présence d’alias
  • Si l’on écrit la même opération maxArray en Zig et en Rust, on obtient presque le même code assembleur haute performance, mais mieux l’intention est exprimée, meilleur est le résultat de l’optimisation
  • Comme on ne peut pas toujours faire confiance aux performances du compilateur, il faut, dans les sections critiques, vérifier directement le code et le résultat de la compilation pour chercher des pistes d’optimisation

Le rôle de Zig

  • Grâce à son explicitation précise, à ses nombreuses fonctions intégrées, à ses pointeurs et annotations, à comptime et à un comportement illégal bien défini, Zig permet de produire du code optimisé sans information abstraite superflue
  • Rust garantit par défaut, grâce à son modèle mémoire, l’absence d’alias entre arguments, alors qu’en Zig il faut ajouter soi-même des annotations comme noalias
  • Si l’on se base uniquement sur LLVM IR, le niveau d’optimisation de Zig est lui aussi élevé
  • Surtout, comptime (exécution à la compilation) est un outil d’optimisation extrêmement puissant dans Zig

Qu’est-ce que comptime ?

  • Le comptime de Zig est utilisé pour la génération de code, l’intégration de constantes, la création de structures génériques basées sur les types, etc., et joue un rôle important dans l’amélioration des performances à l’exécution
  • Il permet d’implémenter de la métaprogrammation
  • Contrairement aux macros de C/C++ ou au système de macros de Rust, comptime n’a pas de syntaxe distincte : c’est du code ordinaire
  • Le code comptime ne modifie pas directement l’AST ; il permet d’inspecter, de refléter et de générer à la compilation pour tous les types
  • La souplesse de comptime a influencé l’évolution d’autres langages comme Rust et s’intègre naturellement au langage Zig

Les limites de comptime

  • Certaines fonctionnalités de macro, comme le token-pasting, ne peuvent pas être remplacées par le comptime de Zig
  • Zig privilégie la lisibilité du code ; il n’autorise donc pas la création de variables ou la définition de macros hors de leur portée
  • En contrepartie, le comptime de Zig offre de nombreux usages en métaprogrammation, comme la réflexion sur les types, l’implémentation de DSL ou l’optimisation du parsing de chaînes

Optimiser la comparaison de chaînes avec comptime

  • Une fonction classique de comparaison de chaînes peut être implémentée dans n’importe quel langage, mais dans Zig, lorsque l’une des deux chaînes est une constante connue à la compilation, il est possible de générer un code assembleur plus efficace
  • Par exemple, si une chaîne vaut toujours "Hello!\n", on peut exploiter une optimisation consistant à la comparer non pas octet par octet, mais par blocs plus larges
  • En utilisant comptime, on peut générer à la compilation du code haute performance exploitant des vecteurs SIMD, un traitement par blocs et des optimisations sur les octets restants
  • Cette approche permet d’implémenter non seulement des comparaisons répétitives de chaînes, mais aussi divers mappings fondés sur des données statiques, des tables de hachage parfaites, des parseurs d’AST, et d’autres composants orientés performance

Conclusion

  • Zig convient très bien à l’optimisation bas niveau et permet d’implémenter directement des performances de tout premier plan grâce à une structure de code explicite et à la puissance de comptime
  • Même comparé à d’autres langages comme Rust, les capacités de programmation à la compilation et l’explicitation de Zig constituent un avantage majeur pour le développement de logiciels hautes performances
  • Les capacités d’optimisation de Zig devraient devenir un atout concurrentiel encore plus important à l’avenir

1 commentaires

 
GN⁺ 2025-06-08
Commentaire Hacker News
  • Ce qui me paraît le plus intéressant dans Zig, c’est la simplicité du système de build, la compilation croisée et la recherche d’une forte vitesse d’itération. Je suis développeur de jeux, donc les performances comptent, mais pour la plupart des besoins, la plupart des langages offrent des performances suffisantes. Ce n’est donc pas mon critère principal de choix d’un langage. On peut écrire du code solide dans n’importe quel langage, mais je vise des frameworks tournés vers l’avenir, maintenables pendant des décennies. Le fait que C/C++ soit pris en charge partout en a fait le choix par défaut, mais j’ai l’impression que Zig peut arriver au même niveau
    • J’ai lancé Zig sur un très vieux Kindle pour le fun (Linux 4.1.15), et j’ai été impressionné par le degré de finition de Zig. Presque tout a fonctionné immédiatement, et j’ai même pu déboguer des bugs étranges avec un vieux GDB. Moi aussi, Zig m’a séduit. On peut voir plus de détails sur cette expérience ici
    • J’ai l’impression qu’on peut écrire du code solide dans la plupart des langages, mais je veux du code modulaire pensé pour durer des décennies. J’aime Zig, mais je pense qu’il a des défauts sur la maintenance à long terme et la modularité. Zig est hostile à l’encapsulation. Il est impossible de rendre privés les membres d’une struct. Ce commentaire d’issue en donne un exemple. La position de Zig est qu’il ne devrait pas exister de représentation interne distincte, et que tous les utilisateurs devraient connaître l’implémentation interne via sa documentation/publication. Mais pour préserver un contrat d’API, au cœur du logiciel modulaire, il faut pouvoir cacher l’implémentation interne, et ce n’est pas possible. J’espère que Zig prendra un jour en charge les champs privés
    • J’ai utilisé Rust un peu à la légère et je l’ai aimé. Mais comme j’entendais dire qu’il était « mauvais », j’ai arrêté un moment avant de le réessayer. Et je l’aime toujours. Je ne comprends pas vraiment pourquoi les gens le détestent autant. La syntaxe générique disgracieuse, C# et TypeScript l’ont aussi. Quant au borrow checker, si on a de l’expérience avec les langages bas niveau, il est plutôt facile à comprendre
    • Zig donne l’impression d’être un Rust plus simple, et un meilleur Go. D’un autre côté, parmi les outils construits avec Zig, j’adore vraiment bun, au point d’être admiratif. bun m’a énormément simplifié la vie. uv, basé sur Rust, procure une expérience similaire
    • Je suis d’accord sur le fait que C/C++ reste la base. La plupart des tentatives pour créer quelque chose de mieux que C ont fini par devenir du C++. Mais il ne faut pas arrêter d’essayer. Rust et Zig sont la preuve qu’on peut encore espérer mieux. Pour ma part, je vais apprendre davantage de C++ à partir de maintenant
  • Même si les compilateurs de pointe cassent parfois la spec du langage, l’hypothèse de Clang selon laquelle une boucle infinie finit par se terminer est correcte selon le standard depuis C11. C11 dit explicitement ceci : « une boucle dont l’expression de contrôle n’est pas une expression constante, et qui n’effectue pas d’opérations d’E/S, volatile, sync ou atomic, peut être supposée terminante par le compilateur »
    • En C++ (jusqu’à C++26 à venir), cette règle s’applique à toutes les boucles, mais comme vous l’avez dit, en C elle ne s’applique qu’aux « boucles dont l’expression de contrôle n’est pas une expression constante ». Autrement dit, une boucle manifestement infinie comme for(;;); doit réellement rester infinie, et loop {} en Rust devrait l’être aussi. Mais les développeurs LLVM semblent parfois croire qu’ils ne construisent qu’un compilateur C++, si bien que quand Rust demande « merci de me laisser une boucle infinie », LLVM répond « impossible en C++, donc optimisation ! », ce qui provoque des problèmes. En somme, une mauvaise optimisation a été appliquée au mauvais langage
  • Même sans fonctionnalité comptime, on peut tout à fait faire en C de l’inlining et de l’unrolling de comparaisons de chaînes. Exemple lié
    • Remarque juste ! Mon premier exemple était trop simple. Un meilleur exemple serait l’automate de suffixes en compile-time. Et le code godbolt lié plus haut montre en fait l’un des deux cas qu’il ne faut justement pas faire
  • Je ne pense pas que la partie qui dit que le bytecode généré pour l’exemple JavaScript est inefficace dans V8 soit une bonne comparaison. On demande à Zig et Rust de compiler en ciblant un environnement très récent, alors qu’on n’impose pas à V8 ce type d’options d’optimisation. En réalité, les JIT modernes peuvent eux aussi vectoriser si les conditions s’y prêtent. Et la plupart des langages modernes traitent les optimisations autour des chaînes de manière similaire. À titre de référence, voici aussi un exemple en C++
    • Comparer JS et Zig revient pratiquement à comparer des pommes et une salade de fruits. L’exemple Zig utilise un tableau de type et de taille fixes, tandis que JS utilise du code « générique » avec des types variés à l’exécution. Pour cette raison, en JS, si on fournit correctement les informations de type, le JIT peut produire des boucles bien plus rapides, même sans aller jusqu’à la vectorisation. En pratique, on n’utilise pas souvent TypedArray, car le coût d’initialisation est élevé et cela ne devient intéressant qu’en cas de réutilisation fréquente. L’article disait aussi que le code JS était gonflé, mais c’est largement parce que le JIT n’ose pas faire confiance aux vérifications de longueur de tableau et ajoute des gardes ; en réalité, tout le monde écrit des boucles du type i < x.length, ce qui permet l’optimisation JIT. C’est donc un peu pinailler, même si la nuance existe
    • On peut aussi modifier les exemples godbolt de Rust et Zig pour cibler un CPU plus ancien. Je n’avais pas pensé à cette limite côté JS. Et l’exemple C++ montre à quel point clang produit du bon code. Cela dit, pour l’instant, l’assembleur n’est pas si satisfaisant que ça, même en tenant compte du fait que Zig est compilé pour un CPU précis. Ce serait vraiment intéressant de voir un portage C++ de l’automate de suffixes en compile-time. C’est un cas concret d’usage de comptime que les compilateurs C++ ne peuvent pas vraiment anticiper
  • Je doute de l’affirmation selon laquelle « les langages de haut niveau manquent de l’intention que possèdent les langages bas niveau ». Au contraire, je pense que la force des langages de haut niveau est justement de pouvoir exprimer l’intention de façon bien plus variée et détaillée
    • Je suis d’accord. Fondamentalement, la différence entre langages de haut niveau et de bas niveau, c’est que dans les premiers on exprime l’intention, tandis que dans les seconds on doit exposer le mécanisme d’implémentation lui-même
    • Ici, par « intention », on ne parle pas d’une intention métier comme « calculer la taxe de cet achat », mais plutôt de quelque chose de plus proche de « décaler ce byte de trois bits vers la gauche », c’est-à-dire ce qu’on demande concrètement à l’ordinateur. Par exemple, du code comme purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?; est rempli d’intention, mais il est impossible de prédire à quoi ressemblera réellement le code machine
  • J’aime vraiment beaucoup le modèle d’allocator de Zig. J’aimerais qu’en Go on puisse utiliser des choses comme des allocators par requête au lieu du GC
    • En Go aussi, les allocators personnalisés et les arenas ne sont pas impossibles, mais leur ergonomie est très mauvaise et il est difficile de les utiliser correctement. Il n’existe pas non plus de moyen, au niveau du langage, d’exprimer ou de faire respecter des règles d’ownership. Au final, on se retrouve à écrire quelque chose qui ressemble à du C avec une syntaxe un peu différente, et sans GC cela devient même plus dangereux que C++
  • Je comprends l’idée de « j’aime la verbosité de Zig », mais honnêtement la formule sonne un peu étrange. C est souvent laxiste à bien des endroits, tandis que Zig, à l’inverse, exige souvent beaucoup trop de « bruit d’annotation » (notamment avec les casts entiers explicites dans les expressions mathématiques). Voir ce billet. Côté performances, si Zig est parfois plus rapide que C, c’est surtout parce que Zig utilise des réglages LLVM plus agressifs (-march=native, optimisation à l’échelle du programme entier, etc.). En réalité, en C aussi, des indices d’optimisation comme unreachable sont possibles via des extensions de langage, et Clang est lui aussi très agressif pour le constant folding. Autrement dit, la différence entre comptime en Zig et la génération de code en C vient souvent des réglages d’optimisation du compilateur. TL;DR : si C est lent, il faut d’abord vérifier les options du compilateur. Au bout du compte, le cœur de l’optimisation reste LLVM
    • Pour l’exemple des casts, on peut au contraire améliorer la réutilisabilité et l’intention du code en créant une fonction qui encapsule le cast
      fn signExtendCast(comptime T: type, x: anytype) T {
        const ST = std.meta.Int(.signed, @bitSizeOf(T));
        const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x)));
        return @bitCast(@as(ST, @as(SX, @bitCast(x))));
      }
      export fn addi8(addr: u16, offset: u8) u16 {
        return addr +% signExtendCast(u16, offset);
      }
      
      Cette approche produit le même assembleur, tout en étant plus réutilisable et plus claire
    • Les idées de Zig sont intéressantes, et on mettait davantage l’accent sur comptime et la compilation de programme entier que ce à quoi je m’attendais en lisant l’article d’origine. Je partage cet avis. À noter que Virgil prenait déjà en charge dès 2006 l’usage d’un langage entier au compile-time et la compilation de programme entier. Virgil ne cible pas LLVM, donc les comparaisons de vitesse reviennent au fond à comparer les backends. Grâce à cette approche, Virgil peut appliquer des optimisations très puissantes : lier statiquement à l’avance les appels de méthodes (devirtualize), éliminer autant que possible les champs/objets inutilisés, propager des constantes jusque dans les objets alloués sur le heap via les champs, ou encore spécialiser parfaitement
    • En pensant à l’usage futur de l’IA, j’ai l’impression que les langages plus explicites et verbeux vont devenir dominants. Qu’on code ou non avec l’IA, et qu’on juge cela bon ou mauvais, beaucoup de développeurs vont préférer être aidés par l’IA, et les langages évolueront en conséquence
    • Si un nouveau backend x86 est introduit, on commencera peut-être à voir des cas où l’écart de performances entre C et Zig viendra vraiment du projet Zig lui-même
    • Concernant les casts entiers explicites, une amélioration plus propre devrait arriver bientôt. Voir cette discussion
  • Faire des benchmarks sur « C est plus rapide que Python » comme si on comparait les langages eux-mêmes n’a pas beaucoup de sens, mais certaines fonctionnalités d’un langage peuvent constituer de gros obstacles à l’optimisation. Avec un langage approprié, le développeur comme le compilateur peuvent exprimer l’intention d’une manière à la fois naturelle et rapide
  • La syntaxe de la boucle for en Zig me paraît beaucoup trop brouillonne. Le fait de devoir placer deux listes côte à côte et aligner les positions me fait mal aux yeux rien qu’à regarder. Je pense que c’est une erreur des langages récents de déverser autant de syntaxe « magique » et de symboles spéciaux. J’aurais du mal à regarder ça pendant des heures
    • Ce motif consistant à parcourir deux tableaux est très courant dans le code bas niveau, et l’itération en parallèle l’est aussi. Donc je trouve au contraire approprié que Zig le prenne en charge de manière claire et naturelle. Je me demande pourquoi cela ferait mal aux yeux
  • L’optimisation est extrêmement importante. Ses effets ne font que croître avec le temps
    • Cela dit, cela suppose bien sûr que le logiciel soit réellement utilisé