1 points par GN⁺ 2026-05-06 | 2 commentaires | Partager sur WhatsApp
  • Async Rust permet d’exécuter le même code, indépendant de l’exécuteur, sur des serveurs et des microcontrôleurs, mais la machine à états générée par le compilateur entraîne une augmentation notable de la taille des binaires, en particulier dans l’embarqué
  • Même un exemple simple comme bar(), avec deux points d’attente, génère 360 lignes de MIR ainsi que les états Unresumed, Returned, Panicked, Suspend0 et Suspend1, alors que la version synchrone n’a besoin que de 23 lignes
  • Remplacer le panic après un nouveau poll d’un future déjà terminé par un retour de Poll::Pending permet de respecter le contrat sans comportement unsafe, et les expériences montrent une réduction de 2 % à 5 % de la taille des binaires sur des firmwares embarqués
  • Même async { 5 }, sans await, construit aujourd’hui une machine à états avec 3 états de base, mais l’optimiser pour renvoyer systématiquement Poll::Ready(5) réduit de 0,2 % la taille des binaires embarqués
  • Le Project Goal proposé vise à faire progresser côté compilateur la suppression du panic après complétion en mode release, la suppression de la machine à états pour les blocs async sans await, l’inlining des futures à await unique, et la fusion d’états identiques

Le problème de gonflement au niveau compilateur dans Async Rust

  • Async Rust permet d’exécuter un même code indépendant de l’exécuteur sur des serveurs et des microcontrôleurs, mais sur les petits microcontrôleurs l’augmentation de la taille des binaires est particulièrement visible
  • Le blog Rust a présenté async/await comme une abstraction à coût nul, mais en pratique async provoque beaucoup de gonflement, y compris sur desktop et serveur ; le problème y est simplement moins visible grâce à des ressources mémoire et CPU plus abondantes
  • Après des contournements pour éviter ce gonflement lors de l’écriture de code async, un Project Goal a été soumis pour traiter le problème dans le compilateur
  • Le problème des futures inutilement volumineux et des copies excessives est exclu du périmètre

Structure des futures générés

  • Dans l’exemple, foo() retourne async { 5 } et bar() exécute foo().await + foo().await
  • bar comporte deux points d’attente, donc la machine à états a besoin d’au moins deux états, mais en pratique le compilateur en génère davantage
  • Le compilateur Rust peut dumper le MIR à plusieurs passes, et la passe coroutine_resume est la dernière passe MIR spécifique à async
    • async existe encore dans le MIR, mais plus dans le LLVM IR ; la transformation d’async en machine à états se produit donc dans les passes MIR
  • La fonction bar génère 360 lignes de MIR, tandis que la version synchrone n’en utilise que 23
  • Le CoroutineLayout produit par le compilateur correspond en pratique à un ensemble d’états sous forme d’enum
    • Unresumed : état initial
    • Returned : état terminé
    • Panicked : état après panic
    • Suspend0 : premier point d’attente, qui stocke le future de foo
    • Suspend1 : second point d’attente, qui stocke le premier résultat et le second future de foo
  • Future::poll est une fonction sûre ; même si on l’appelle de nouveau après la fin d’un future, cela ne doit pas provoquer d’UB
    • Aujourd’hui, après Suspend1, il renvoie Ready puis fait passer le future à l’état Returned
    • Un nouveau poll dans cet état provoque un panic
  • L’état Panicked semble servir à empêcher qu’un future soit repollé après qu’une fonction async a paniqué puis que ce panic a été intercepté avec catch_unwind
    • Après un panic, le future peut être dans un état incomplet ; le repoller pourrait donc mener à de l’UB
    • Ce mécanisme ressemble beaucoup au mutex poisoning
    • Cette interprétation de l’état Panicked n’est pas documentée de manière totalement claire ; le niveau de confiance annoncé est d’environ 90 %

Faut-il vraiment paniquer lors d’un poll après complétion ?

  • Les futures à l’état Returned paniquent actuellement, mais ce n’est pas une obligation
    • La seule contrainte nécessaire est d’éviter toute UB
  • Un panic est relativement coûteux et ajoute un chemin à effets de bord difficile à éliminer par optimisation
  • Si un future déjà terminé retourne Poll::Pending lorsqu’il est repollé, le contrat du type Future peut être respecté sans comportement unsafe
  • Après modification du compilateur pour tester cette approche, une réduction de 2 % à 5 % de la taille des binaires a été observée sur des firmwares embarqués async
  • Il est proposé d’exposer ce comportement via un switch, à la manière de overflow-checks = false pour les dépassements d’entiers
    • En build debug, le panic resterait en place pour révéler immédiatement un comportement incorrect
    • En build release, on obtiendrait des futures plus petits
  • Avec panic=abort, il serait peut-être possible de supprimer complètement l’état Panicked, mais l’impact doit être étudié plus en détail

Une machine à états est générée même sans await

  • foo() ne retourne que async { 5 }, donc la forme optimale en implémentation manuelle serait un future sans état qui retourne toujours Poll::Ready(5)
  • Pourtant, le MIR généré par le compilateur contient toujours les trois états de base Unresumed, Returned et Panicked
    • Lors du poll, le discriminant de l’état courant est vérifié puis un branchement est effectué
    • Si le future est repollé après sa complétion, cela déclenche un panic avec `async fn` resumed after completion
  • Dans ce cas, on peut optimiser en supprimant totalement la machine à états et en retournant simplement Poll::Ready(5) à chaque appel
  • Une application expérimentale de cette idée dans le compilateur a réduit de 0,2 % la taille des binaires embarqués
    • Le gain est modeste, mais l’optimisation est simple, donc elle peut valoir le coup
  • Cette optimisation modifie légèrement le comportement, mais seuls les exécuteurs qui ne respectent pas le contrat seraient affectés
    • Aujourd’hui, le compilateur déclenche un panic lors d’un poll ultérieur
    • Après optimisation, le future retournerait toujours Ready

LLVM seul ne suffit pas

  • Même si le MIR produit est inefficace, LLVM peut parfois tout nettoyer, mais seulement sous certaines conditions
    • Le future doit être suffisamment simple
    • Il faut utiliser opt-level=3
  • Dès que le future devient plus complexe, LLVM n’élimine plus tout ; or dans le code Async Rust idiomatique, les futures sont profondément imbriqués, ce qui fait rapidement exploser la complexité
  • Dans les environnements où l’on optimise fréquemment pour la taille, comme l’embarqué ou wasm, LLVM n’arrive pas à tout optimiser
  • Exemple Godbolt : https://godbolt.org/z/58ahb3nne
    • Dans l’assembleur généré, LLVM comprend bien que foo retourne 5, mais n’optimise pas le résultat de bar en 10
    • L’appel à la fonction poll de foo est toujours présent
    • Cela vient de chemins de panic potentiels que le compilateur ne parvient pas à écarter complètement
    • LLVM ne sait pas que foo n’est réellement appelé qu’une seule fois et qu’il ne panique pas
  • Si l’on commente la branche de panic dans l’IR, l’optimisation devient meilleure : https://godbolt.org/z/38KqjsY8E
  • Plutôt que d’attendre une optimisation a posteriori de LLVM, le compilateur doit fournir à LLVM une meilleure entrée

L’inlining des futures fonctionne mal

  • L’inlining est important parce qu’il ouvre la voie aux passes d’optimisation suivantes, mais les futures générés par Rust ne sont actuellement pas inlinés à un stade suffisamment précoce
  • Chaque future obtient bien une implémentation, puis LLVM et l’éditeur de liens ont une occasion de l’inliner, mais à cause des problèmes précédents cela arrive trop tard
  • Le cas d’inlining le plus direct est celui où bar() ne fait que foo(blah).await
    • C’est un motif fréquent quand on construit des abstractions avec des traits
    • Aujourd’hui, le compilateur génère une machine à états pour bar, puis appelle à l’intérieur la machine à états de foo
    • Une approche plus efficace serait que bar soit directement le future de foo
  • Lorsqu’il existe un préambule et un postambule, le cas devient plus complexe
    • Exemple : bar(input) construit blah avec input > 10, puis exécute foo(blah).await avant d’appliquer * 2 au résultat
    • C’est fréquent lors de la transformation d’une fonction async vers une autre signature, notamment dans des implémentations de trait
  • Même sous cette forme, bar n’a pas besoin de son propre état async
    • Aucune donnée, en dehors de la valeur capturée par foo, n’a besoin d’être conservée au-delà de l’unique point d’attente
    • En revanche, bar ne peut pas être simplement remplacé par foo lui-même ; il peut surtout s’appuyer sur l’essentiel de l’état de foo
  • Dans une implémentation manuelle, BarFut pourrait avoir les états Unresumed { input } et Inlined { foo: FooFut }
    • Au premier poll, il exécute le préambule, crée foo(blah) puis passe à l’état Inlined
    • Ensuite, il applique le postambule au résultat de foo.poll(cx)
  • On pourrait aussi supprimer l’état Unresumed si l’on exécutait le code avant le premier point d’attente à l’avance, mais le contrat garantit qu’un future ne fait rien tant qu’il n’a pas été poll
  • Si l’on pouvait interroger les propriétés d’un future en cours de poll, d’autres optimisations d’inlining deviendraient possibles
    • Par exemple, si l’on sait qu’un future retourne toujours immédiatement ready au premier poll, le future appelant n’a pas besoin de créer un état pour ce point d’attente
    • En appliquant récursivement cette optimisation, beaucoup de futures pourraient être réduits à des machines à états bien plus simples
  • Dans l’architecture actuelle de rustc, chaque bloc async semble être transformé séparément et les données utiles ne sont pas conservées ensuite, ce qui empêche ce type d’interrogation
  • L’inlining des futures n’a pas encore été expérimenté, mais il devrait apporter des gains importants en taille de binaire et en performances

Fusion d’états identiques

  • Chaque point d’attente d’un bloc async ajoute un état supplémentaire à la machine à états
  • Un code comme le suivant est naturel, mais comme les deux branches attendent la même fonction async, il crée deux états identiques
    • CommandId::A => send_response(123).await
    • CommandId::B => send_response(456).await
  • Dans ce cas, le CoroutineLayout crée _s0 et _s1, qui stockent chacun le même type de coroutine de send_response, ainsi que deux états Suspend0 et Suspend1
  • Le MIR de cette fonction comporte 456 lignes, et nombre de blocs de base sont en pratique des doublons
  • En refactorisant manuellement le code pour calculer d’abord la valeur de réponse, puis faire un seul send_response(response).await, ces états redondants disparaissent
    • CommandId::A donne 123
    • CommandId::B donne 456
    • puis send_response(response).await
  • Après refactorisation, le CoroutineLayout ne contient plus qu’un seul future stocké et un unique état Suspend0
  • La longueur totale du MIR tombe à 302 lignes, et la duplication disparaît
  • Une passe d’optimisation capable de détecter et de fusionner les chemins de code et états identiques semble donc utile
    • Cette optimisation pourrait très bien se combiner avec une passe d’inlining des futures

Liens d’expérimentation et benchmarks supplémentaires

Appel à soutien pour le Project Goal

  • Ce travail a été soumis comme Project Goal afin d’avancer côté compilateur : https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
  • Sans financement, il sera difficile de faire beaucoup progresser le chantier ; un soutien partiel ou total d’entreprises ou d’organisations qui en bénéficieraient est donc nécessaire
  • Le contact indiqué est dion@tweedegolf.com
  • Le périmètre du travail et le montant nécessaire sont flexibles, mais 30 k€ seraient, d’après l’estimation, suffisants pour réaliser tout ou une grande partie du projet

2 commentaires

 
GN⁺ 2026-05-06
Commentaires Hacker News
  • Je suis d’accord pour dire que le titre est un peu exagéré, mais l’article est bien écrit et l’idée principale passe bien
    Je n’ai pas encore assez d’expérience avec l’async en Rust pour avoir un avis tranché, mais quelques points m’ont marqué
    Le bon côté, c’est qu’on peut avoir un runtime explicite. Au lieu de contaminer tout le projet avec l’async, on peut garder une base synchrone et n’utiliser le runtime qu’aux « frontières » des entrées/sorties
    Sur le projet sur lequel je travaille, cette approche a bien fonctionné, et elle ressemble assez à la stratégie adoptée par Zig pour le code d’E/S. Dans ce cas, le problème de la couleur des fonctions était en grande partie résolu, et comme il fallait séparer strictement les E/S du code centré CPU, un runtime d’E/S explicite paraissait naturel
    Le mauvais côté, c’est que tout l’écosystème semble trop dépendre de tokio. C’est un peu comme si le GC de Java était optionnel, mais qu’en pratique tout le monde utilisait le même runtime GC tiers, et que n’importe quelle bibliothèque l’impose. Ce type de dépendance centrale n’est pas sain

    • Selon le contexte, on peut avoir l’impression que tout l’écosystème dépend de tokio, mais en regardant Rust embarqué, c’est plus compréhensible
      Les exigences d’un runtime async sur un processeur de station de travail et dans un environnement comme le RP2040 sont très différentes. Malgré cela, comme on peut changer de backend, même en écrivant du code d’E/S async pour un petit microcontrôleur ARM M0, avec embassy — un runtime centré embarqué — le code ressemble presque à celui utilisé ailleurs
      Comme on utilise les mêmes traits et interfaces, on peut moins se soucier des détails du runtime. Comparé à l’utilisation d’un petit RTOS ou à la création de son propre environnement async, c’est plutôt bien
      Et ce qu’on apprend en écrivant du code async avec embassy se transfère aussi à d’autres domaines
    • Je me demande quelle est l’alternative. Je suis satisfait d’utiliser tokio, mais c’est aussi très bien que d’autres utilisent smol, async-std, glommio et d’autres exécuteurs
      Même si tokio ne fait pas partie de la bibliothèque standard, il est bien maintenu, donc la situation actuelle me paraît correcte. Au contraire, si cela entrait dans la bibliothèque standard, j’aurais peur qu’il devienne plus difficile d’utiliser d’autres exécuteurs, et plus compliqué aussi de porter la bibliothèque standard sur d’autres plateformes
      Bien sûr, il est possible que cette inquiétude soit infondée
    • C’est intéressant de mentionner Java, parce que Java a historiquement connu des problèmes similaires
      Le logging est aujourd’hui plus ou moins unifié autour de slf4j, mais il reste des bibliothèques qui utilisent autre chose, et pour les utilitaires communs, c’était d’abord Apache Commons puis aujourd’hui souvent Guava
      Pour le JSON, Jackson s’est imposé dans une certaine mesure, mais Gson et Simple-json restent courants, et pour les annotations de nullabilité, on est passé de distributions non officielles du JSR-305 jamais officialisé au checker framework, puis récemment à JSpecify
      Ce genre de briques de base devrait être fourni par le langage afin d’éviter la fragmentation et la prolifération de bibliothèques standard de fait
    • Il y a beaucoup de domaines où l’on peut faire du Rust avec async sans dépendre de tokio. En réalité, ce qui semble vraiment lié à tokio, c’est surtout le web / serveur
      Écrire des bibliothèques indépendantes de l’exécuteur n’est pas très difficile, mais cela demande une vigilance constante, et ce n’est pas quelque chose que l’ensemble de la communauté respecte toujours
  • Excellent article. J’aime ce type d’analyse d’optimisation en profondeur, et j’espère aussi que les objectifs du projet aboutiront bien
    J’ai souvent l’impression que le compilateur ne consacre pas beaucoup d’efforts à l’optimisation des cas « triviaux »
    Cela dit, le titre est trop dramatique par rapport au contenu. J’aurais aussi cliqué sur « Async Rust Optimizations the Compiler Still Misses »

    • Le titre a été choisi ainsi parce qu’il est simplement factuel. Depuis l’arrivée d’async vers 2019, pas grand-chose n’a vraiment changé
      On peut maintenant utiliser async dans les traits et les closures, mais c’est une mise à jour du système de types, pas un changement de la mécanique async elle-même. Les Waker sont aussi devenus un peu plus faciles à manipuler, mais cela relève davantage d’améliorations côté std/core
      Si j’ai bien compris, les personnes qui ont fait atterrir l’async en Rust ont pas mal souffert de burnout et sont devenues moins actives, et presque personne n’a vraiment pris le relais. Cela dit, je suis assez content que des gens de Google aient ouvert une PR pour optimiser la disposition mémoire des variables capturées
      Mes collègues et moi utilisons beaucoup l’async, donc il se peut qu’on doive le faire nous-mêmes, ou au moins s’y mettre. On est peut-être plus proche du sens où « gratuit » signifie « comme un chiot gratuit »
      Donc oui, le titre est un peu putaclic, mais je n’ai pas l’intention de le retirer
    • Je suis d’accord pour dire que le titre est trop exagéré
      L’auteur semble obsédé par l’overhead de fonctions triviales. Il est gêné par l’overhead des états « panic » et « returned », mais ce n’est pas un gros problème
      La plupart des blocs async utiles sont suffisamment gros pour que l’overhead des cas d’erreur soit noyé dedans
      Il y a peut-être quelque chose à dire sur le manque d’inlining. Mais ce qui limite le nombre massif d’activités, c’est en général l’espace d’état requis par chacune
  • L’async me semble globalement être une idée encore immature. Le code ordinaire était déjà asynchrone
    Si on doit attendre une tâche async, le thread dort jusqu’à ce qu’elle soit prête, et le noyau abstrait cela. Mais comme on n’aimait pas structurer le code avec des threads logiques, on a ajouté un système de callbacks pour les événements, puis on s’est rendu compte que les callbacks sont difficiles à raisonner et que le contrôle séquentiel est préférable
    Donc je pense que les threads étaient le bon modèle de programmation
    Maintenant, les runtimes de langage préfèrent les « green threads » pour des raisons de portabilité et de performance, mais la plupart des langages ne les fournissent pas correctement. À la place, on se retrouve avec les problèmes de couleur async/non-async, d’ordonnancement, de priorité, de non-préemption. C’est un modèle d’ordonnancement et de processus pire que dans les années 1970

    • Dire que « le code ordinaire était déjà async, et que le thread dort quand il attend, avec le noyau qui abstrait cela » n’est pas exact
      Même le code async est souvent écrit de manière à ne pas maximiser le parallélisme exprimable. Par exemple, on écrit « await process(x) pour chaque tâche X » au lieu de « lancer simultanément les N tâches d’E/S »
      Mais dans le monde des threads, ce problème de parallélisme est encore pire. Les threads sont intrinsèquement trop lourds pour exprimer efficacement la concurrence, et il n’y a aucun moyen de les optimiser dans cette direction
      Ce n’est pas une leçon nouvelle. On sait depuis longtemps que les exécuteurs à work-stealing ont une latence bien plus faible et des P99 plus stables que les threads traditionnels. C’est d’ailleurs pour cela qu’Apple a créé GCD au début des années 2000
      Les threads ne fournissent pas au scheduler du noyau les informations plus riches dont il aurait besoin pour comprendre la charge de travail, et les threads noyau sont un mécanisme bien trop lourd pour obtenir une concurrence fine. C’est encore pire quand il ne s’agit pas de calcul pur mais d’E/S ou de charges mixtes
      Tous les programmes n’ont pas besoin de ce niveau de performance, mais à effort égal, il est beaucoup plus simple d’atteindre un niveau de performance supérieur, et en pratique on obtient des latences et des débits que l’approche traditionnelle a du mal à suivre
      Un autre signal que l’async va dans la bonne direction se voit avec io_uring. L’approche haute performance des E/S dans le noyau avec io_uring est totalement différente du threading traditionnel et des appels système, et le traitement des complétions ressemble bien davantage à la concurrence async. Cela dit, async/await à lui seul n’a pas assez de « couleurs » pour exprimer les relations entre tâches async, donc l’exploiter pleinement reste plus difficile
    • Dès que le noyau et l’ordonnanceur de l’OS interviennent, on peut devenir 3 à 4 ordres de grandeur plus lent que ce qui devrait être possible
      La dernière fois que j’ai travaillé sur du code de coroutines / scheduling, créer un thread qui se termine immédiatement puis faire un join prenait environ 200µs, alors que créer, planifier puis attendre un green thread maison prenait environ 400ns
      Pas besoin d’attendre 10 ans qu’un autre conçoive un framework async absurdement complexe. Dans n’importe quel langage système, 20 lignes d’assembleur suffisent pour faire ses propres green threads / coroutines avec pile
    • Que « les threads soient le bon modèle de programmation » dépend de ce qu’on fait. Pour les charges centrées calcul, les threads sont adaptés, et pour les charges centrées bande passante, l’async l’est davantage
      L’optimisation du code centré bande passante est une question de conception de l’ordonnancement. Dans le modèle multithread classique, on ne contrôle le scheduling que de manière limitée, alors que dans un modèle async on peut le contrôler presque parfaitement
      Un ordonnanceur async bien optimisé est énormément plus rapide qu’une architecture multithread équivalente sur ce type de charge centrée bande passante, à un point qui n’est même pas comparable
      Aujourd’hui, la plupart du code haute performance est centré bande passante, et l’async existe pour optimiser plus facilement ce type de charge
    • Je trouve au contraire que les callbacks sont plus faciles à raisonner
      Quand on teste le traitement concurrent et qu’on vérifie si les race conditions sont correctement gérées, les callbacks rendent cela bien plus simple parce qu’ils permettent de contrôler l’ordonnancement. Chaque callback représente une unité distincte, donc on voit quels événements peuvent être réordonnés, et on peut plus facilement examiner différents ordres
      Avec les threads, au contraire, il est facile d’ignorer l’ordre, et de ne pas réfléchir à quel moment la complexité produite par les autres threads peut affecter le thread courant. Ce n’est pas vraiment plus simple, c’est plutôt une simplification trompeuse
      Et sauf à ajouter des barrières artificielles pour bloquer les threads, ou à remplacer les E/S par des stubs et injecter des mocks avec callbacks pour contrôler l’ordre, il est difficile de réellement faire varier les scénarios concurrents dans les tests
      Le problème des callbacks, c’est que la pile d’appels capturée n’est pas la pile d’appels logique. À moins d’utiliser certaines bibliothèques/runtimes qui ont travaillé à rendre cela utile, il faut une bonne définition des erreurs
      Bien sûr, on peut aussi mélanger les deux paradigmes et n’en récupérer que les défauts
    • Les threads ne sont ni meilleurs ni pires que async+callbacks, c’est simplement un autre modèle. Il y a des problèmes qui se prêtent bien aux threads, et d’autres qui s’expriment bien mieux en async
  • Si l’objectif principal de Rust est la sûreté, je ne comprends pas pourquoi il y a panic. On devrait pouvoir prouver qu’il n’existe absolument aucun chemin pouvant paniquer dans le code
    J’ai regardé cela toute la semaine, et il est très difficile de produire un programme dont on peut garantir qu’il ne paniquera jamais. D’après ce que je comprends, le panic handler pèse environ 300 Ko, et le seul moyen de l’exclure est qu’il n’existe aucun chemin de panic possible dans le code à la compilation. Vérifier après compilation si le binaire contient le panic handler ressemble à un hack
    On peut interdire unwrap et les autres opérations qui paniquent via des lints, mais s’il existait un sous-ensemble no-panic de Rust, une bonne partie des problèmes abordés dans cet article disparaîtrait
    Il est frustrant d’avoir affaire à un langage où trop d’opérations peuvent théoriquement paniquer, alors qu’en pratique cela n’arriverait qu’au niveau d’un bit flip ou équivalent. C’est pareil quand il faut prouver qu’un tableau n’est pas vide ou quand on manipule l’async
    Au final, on se retrouve soit à ajouter énormément de gestion d’erreurs pour des situations qui ne se produiront jamais, soit à utiliser des structures étranges comme le motif de liste non vide avec un premier champ et le reste séparé. Et cette structure ajoute elle-même son propre gonflement

    • Le travail autour de Rust-in-Linux traite ce problème avec des choses comme les opérations mémoire faillibles. Pour eux, c’est une fonctionnalité nécessaire
      Des travaux progressent aussi lentement pour augmenter les usages fondés sur des preuves, y compris la capacité à prouver qu’un tableau n’est pas vide
    • panic est assez important pour l’ergonomie et la sûreté
      S’il n’y avait pas de panic et qu’il fallait continuer l’exécution dans tous les cas, il faudrait ajouter énormément de gestion d’erreurs partout où l’on vérifie des invariants pour tenter de récupérer de situations comme une corruption mémoire ayant cassé ces invariants
      Ce serait exactement le même type de problème que celui qui vous inquiète : une énorme masse de gestion d’erreurs pour des situations qui n’arriveront pratiquement jamais
    • L’objectif de Rust, c’est la sûreté mémoire. panic est totalement sûr du point de vue de la sûreté mémoire
    • Même l’OS qui exécute votre programme n’est pas parfait
      Il y a quelque chose de fatigant dans cette attitude qui consiste à vouloir qu’un outil rende tout infalsifiable sans rien faire soi-même. On veut une API facile, puis si ce n’est pas assez facile on veut des conteneurs Kubernetes « programmés » en YAML, et si ce n’est toujours pas assez facile on veut les services d’hébergement à clics de GCP ou Amazon
      Au fond, ce n’est plus vraiment vouloir programmer, c’est vouloir consommer des applications qui ne tombent jamais en panne, et ce mode de vie repose seulement sur une relation symbiotique avec les gens qui fabriquent réellement les choses
  • Ce type de discussion laide mais nécessaire existe aussi depuis longtemps en C++
    Dès l’introduction d’async en Rust, son caractère contagieux ne me plaisait pas
    J’espère que Rust réussira, et si davantage de personnes comme celles-ci s’y mettent, l’avenir de Rust pourrait être plus prometteur

  • J’ai commencé récemment un travail sur l’async en Rust, et le principal problème que je rencontre pour l’instant, c’est la duplication de code
    Chaque fonction pour laquelle je veux supporter à la fois une API asynchrone et une API bloquante doit être écrite en double. Quelque chose comme maybe-async serait appréciable
    J’ai regardé des crates comme maybe-async ou bisync pour contourner cela, mais elles avaient toutes des problèmes ou de fortes contraintes

    • Un travail est en cours sur les génériques de mots-clés, qui permettraient de rendre les fonctions génériques par rapport à des mots-clés comme async ou const
      Aujourd’hui, le meilleur choix pour écrire du code que l’on veut faire vivre à la fois dans le monde synchrone et asynchrone, c’est le sans-io. Thomas Eizinger de Fireguard a écrit un bon article sur ce pattern[1]
      Ce pattern ne résout pas proprement seulement le problème sync/async, il facilite aussi les tests et ouvre la voie à des techniques comme DST[2]
      J’ai aussi écrit un article sur le sujet[3], où j’insiste sur le fait que le problème dépasse la simple opposition async vs sync et englobe plus largement les différents exécuteurs
      0: https://github.com/rust-lang/effects-initiative
      1: https://www.firezone.dev/blog/sans-io
      2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
      3: https://hugotunius.se/2024/03/08/on-async-rust.html
    • Cela dépend énormément de ce que vous faites réellement, mais si c’est suffisamment simple, on peut peut-être écrire une macro qui remplace les types et les await
    • C’est le classique problème de la couleur des fonctions. https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • De mon point de vue, une fonction async est déjà une forme de maybe-async
      La différence entre fn -> void et fn -> Future, c’est que la première s’exécute immédiatement jusqu’au bout, alors que la seconde peut ne se terminer que plus tard
      Si vous voulez exécuter une fonction async de manière bloquante, il suffit d’utiliser un exécuteur bloquant
  • J’aime cet article parce qu’il m’a aussi fait découvrir les objectifs Rust 2026
    Mon équipe utilise Rust, mais nous n’avons pas eu besoin d’entrer très profondément dans le langage pour faire ce dont nous avions besoin. Cela dit, c’est agréable de voir un langage se développer à partir de sa base avec autant de retours de la communauté
    Je n’ai jamais vraiment eu cette impression avec le C++, et je ne sais pas très bien comment cela se passe dans d’autres domaines
    Le seul point un peu décevant, c’est que chaque objectif semble demander un financement spécifique, ce qui donne un petit côté Kickstarter. Je me demande si c’est vraiment le meilleur modèle trouvé jusqu’ici

    • Le terme « objectif de projet » est assez trompeur par rapport à ce qu’il signifie réellement
      Un objectif de projet est un système qui permet à une personne ou à un petit groupe d’exprimer qu’ils veulent travailler sur quelque chose, et de demander aux bénévoles du projet Rust un soutien durable en temps, comme des revues de code ou des réponses à des questions
      Cela ne veut pas dire que le projet Rust lui-même s’est fixé cet objectif, ni qu’il le soutient nécessairement
      Il n’est donc pas correct de le voir comme une feuille de route officielle de Rust ; il vaut mieux y voir quelque chose comme « il y a des contributeurs qui veulent travailler dans ce domaine »
    • Il semble exister un certain consensus, au sein même du comité ISO du C++, sur le fait que le processus d’évolution du langage est dans une certaine mesure cassé. Principalement à cause de sa taille et de sa manière de s’organiser
      Malheureusement, une fois qu’une technologie s’installe commercialement, cela semble souvent évoluer de cette façon. Il est difficile d’en vouloir aux gros sponsors de ne financer que les aspects qui les intéressent
      Heureusement, une part importante du financement de TweedeGolf viendrait du gouvernement néerlandais
    • Il semble y avoir grosso modo deux types de travail open source : le développement de fonctionnalités et la maintenance
      Les nouvelles fonctionnalités, cela se « vend ». Leur développement coûte de l’argent, mais elles résolvent de vrais problèmes, et si le coût de ces problèmes dépasse celui du développement, les entreprises sont généralement prêtes à payer
      La maintenance est plus difficile, mais il existe désormais aussi des fonds pour les mainteneurs. Le fonds de RustNL en est un exemple : https://rustnl.org/maintainers/
      Ces fonds visent un travail plus large et plus durable, soutenu par les petites contributions de plusieurs organisations
      Je ne sais pas si c’est le meilleur modèle, mais au moins cela semble fonctionner dans une certaine mesure
  • En lisant la documentation de Rust Async et de Tokio, on voit bien expliqué pourquoi il ne faut pas mettre les parties CPU-intensives dans la pile async, comment utiliser efficacement des outils de base comme std::sync::Mutex dans des blocs async, et comment raccorder du code synchrone et du code async
    Beaucoup de code ne suit pas ces recommandations parce que l’efficacité n’est ni une priorité ni une nécessité. Mais il existe de nombreux projets où la performance et l’efficacité comptent, et une fois le code en production, on finit par voir les pièges. ScyllaDB en est un exemple
    Les LLM n’aident pas non plus. Ils rendent tout async jusqu’à main, utilisent les mauvais outils de base et ne conçoivent pas correctement le système

  • Le repli d’états dupliqués, c’est-à-dire le pattern qui consiste à remonter le match hors des branches await comme dans l’exemple process_command, est probablement la chose la plus simple que n’importe qui puisse appliquer dès aujourd’hui sur du code async existant
    Cela ne demande aucun travail du compilateur, seulement du refactoring

    • Il faudrait au minimum un lint personnalisé qui repère où cela peut s’appliquer. À ce niveau-là, on n’est déjà plus très loin d’un travail sur le compilateur
  • À propos de « les Future ne s’inlinent pas facilement », dans le langage de programmation que j’ai créé, j’ai écrit un pass personnalisé qui inline les appels de fonctions async dans des fonctions async
    Globalement cela marche bien et permet d’éliminer un peu de boilerplate, mais la taille du binaire résultant augmente beaucoup
    Techniquement, Rust pourrait faire la même chose

 
GN⁺ 2026-05-06
Avis sur Lobste.rs
  • C’était un article bien plus constructif que ce à quoi je m’attendais en voyant seulement le titre

    • Je pense que c’est tout simplement assez proche de la réalité. 7 ans après la sortie de la MVP, il n’y a presque pas eu de progrès ni dans la conception du langage ni dans l’implémentation du compilateur, et comme les personnes qui ont principalement produit cette MVP ont réduit leur implication dans le projet à peu près à la même période, la transmission s’est ensuite interrompue
      J’espère que celles et ceux qui veulent travailler là-dessus recevront le soutien nécessaire
  • I want to work on this in the compiler and as such have submitted it as a Project Goal

    Stop generating statemachines that don’t have to be there
    Make the compiler’s job easier by removing panic paths and branches
    Make statemachines smaller

    Je suis content de voir que ce problème est traité. J’ai déjà vu plusieurs fois des billets expliquant qu’en ce moment rustc transmet beaucoup trop de code à LLVM en espérant que l’optimiseur règle tout, et cet article demande en plus un financement pour ce travail

  • Mon Dieu, j’étais idiot
    J’ai toujours pensé que l’async était intrinsèquement « volumineux », puisqu’il faut forcément, sous une forme ou une autre, un runtime, un suivi des tâches et du polling pour vérifier l’achèvement. Donc cette surcharge n’est pas nulle
    Je pensais que la « zéro-cost abstraction » dont on parlait concernait la fonctionnalité du langage, indépendamment du runtime ajouté
    Il ne m’était même jamais venu à l’esprit de regarder ce que rustc émet avant de le passer à LLVM

  • Pour les personnes peu familières avec async Rust :

    It's amazing how we can write executor agnostic code that can run concurrently on huge servers and tiny microcontrollers.

    C’est tout à fait vrai. Même un arbre imbriqué d’appels async se fige, après optimisation maximale, en une seule struct avec une machine à états en interne. C’est vraiment ingénieux

  • Si on atteint ce cas en build de release, est-ce qu’on obtient une sorte de deadlock ? Ou bien y a-t-il aussi un risque de fuite à cause de tâches qui attendent un travail qui reste toujours sur Pending ?

    • Oui. Ces futures se retrouvent alors bloquées et ne se terminent jamais. Cela dit, cet état n’est atteignable qu’à partir de code async bas niveau déjà bogué, et un code incapable de suivre correctement un future terminé produit probablement déjà des fuites et des deadlocks
      On ne peut pas faire un polling incorrect avec .await
  • Quelques réflexions :

    1. Cet article ressemble à un plaidoyer pour déplacer davantage de logique d’optimisation hors de LLVM vers la couche MIR. Par exemple, je comprends pourquoi l’inlining des fonctions async y serait plus simple que dans LLVM. Si on a réussi cela pour l’async au niveau MIR, je me demande s’il ne serait pas possible de généraliser cette logique aux fonctions synchrones aussi, puis de supprimer certains passes d’optimisation de LLVM. Je sais que c’est un gros chantier, et c’est moins une question pratique qu’une question de direction. Une fois qu’un compilateur frontend/middle-end atteint un certain niveau de complexité, il semble possible qu’il soit préférable d’y déplacer une bonne partie des optimisations génériques de LLVM
    2. Je n’aime toujours pas panic=unwind. En dehors de certains test harnesses, je vois rarement des avantages qui compensent son coût par rapport à panic=abort. Même pour les test harnesses, il me semble qu’on pourrait faire un choix similaire sous Linux, de manière un peu obscure, avec clone pour faire un wait sur le thread d’exécution au lieu d’utiliser pthread_join. Je peux me tromper là-dessus
  • Le lien a aussi cessé de marcher chez d’autres personnes ?
    Édition : le billet de blog s’affiche une demi-seconde puis bascule sur une page 404
    Édition 2 : je suis passé par la liste des billets du blog et j’ai cliqué un peu partout, et même en ouvrant cet article depuis la liste, j’arrive sur une page 404. Comment peut-on casser à ce point un blog statique, ou du moins un blog qui devrait l’être ?

    • Le ton paraît un peu inutilement grossier et agressif. Les sites web peuvent aussi avoir des bugs, et le signaler est utile, mais ce commentaire sonne un peu mesquin
      Pour ce que ça vaut, j’ai suivi ce qui semble être les mêmes étapes de reproduction et je n’ai jamais obtenu de 404. J’ai essayé sur téléphone et sur desktop, avec JavaScript activé et désactivé. Il est donc possible que ce que tu as rencontré soit plus complexe que ça en avait l’air