J’aime Rust depuis longtemps ; ses points forts sont évidents, tout comme ses points faibles. Il offre une sécurité mémoire sans GC, 零成本抽象, 并发安全, probablement le meilleur 工具链 et 配套外围建设 du moment, une couverture impressionnante (des serveurs cloud Web aux microcontrôleurs), et une conception de la gestion des erreurs excellente. Les inconvénients ? Une courbe d’apprentissage abrupte, la difficulté à comprendre Ownership, Borrow et d’autres contraintes de modèle, et le fait d’écrire du code est lent.
Depuis longtemps, nous considérons cela comme une faiblesse de Rust, mais en réalité les deux premiers problèmes peuvent être résolus en entraînant notre réflexion et notre cognition. En revanche, la lenteur de la compilation freine constamment mon flux de travail. Il y a quelques années, cela semblait acceptable ; avant que l’IA ne s’impose, j’avais de la patience et je passais souvent des algorithmes ou des fonctionnalités jusqu’au petit matin. Mais aujourd’hui ? Je peine à dire que je n’ai pas changé. Je suis devenu complètement impatient, je n’ai plus envie de réfléchir à la façon d’écrire du code Rust, je ne peux plus attendre la longue barre de progression de Cargo, surtout lorsque l’étape ld n’affiche aucune progression et se bloque — c’est vraiment désagréable.
N'y a-t-il vraiment aucun moyen de remédier à tout cela ? En fait, si. Il y a environ un mois, j'ai essayé quelques méthodes pour accélérer mon processus de développement Rust, y compris l'optimisation du workflow et autres, mais en réalité, le temps d'attente restait à chaque fois bloqué au niveau de Cargo. Si l'on ne résout pas cela, on ne s'attaque pas à la racine du problème. C'est donc l'occasion idéale de parler un peu de l'optimisation de la configuration de Cargo.toml dans le développement Rust et des configurations périphériques associées.
Théorie du backend
Dans la conception de Rust, LLVM est un peu paresseux, mais c’est un bon choix ; contrairement à Go qui maintient lui‑même le processus de génération du code machine, confier les optimisations matures à l’écosystème vaste d’LLVM fonctionne très bien. Le flux de compilation de Rust peut être décrit grossièrement ainsi : le code source subit d’abord une analyse lexicale puis syntaxique pour produire un AST, suivi d’une expansion des macros et d’un déclassement du HIR qui le aplatit. Ensuite, on effectue les vérifications de types classiques ainsi qu’un léger contrôle d’emprunt, pour finalement générer le MIR ; jusqu’à ce point, tout est géré par le front‑end du compilateur Rust. Après le MIR, le travail est confié à LLVM, qui traduit le MIR en LLVM IR, exécute une série de passes d’optimisation (de O0 à O3), puis produit le code machine de la plateforme cible, lequel est ensuite lié pour former un fichier exécutable BIN.
Alors, où se situe exactement le problème ? La réponse est en réalité très simple : LLVM est une infrastructure très lourde — et être lourde est une bonne chose en environnement de production, car échanger du temps de compilation contre du temps d'exécution est une excellente philosophie —, et elle sert de pont pour Rust au niveau supérieur, ainsi que pour toute une foule de langages comme C, C++ et Swift. Cela signifie que son pipeline d'optimisation est conçu pour « générer un code machine optimal » et non pour « achever rapidement la compilation ». Exécuter ces dizaines de passes d'optimisation en mode release est extrêmement chronophage ; même en mode debug à O0, LLVM doit toujours parcourir l'ensemble du processus de génération d'IR et de code machine, un processus qui n'a rien de léger en soi. De plus, la monomorphisation des génériques de Rust développe une quantité massive de code au moment de la compilation, de sorte que le volume d'IR transmis à LLVM est bien plus grand que ce que laisse penser le code source, et la charge de travail que LLVM doit traiter augmente naturellement en conséquence.
Cranelift
En substance, une grande partie du temps de compilation lent n’est pas consacré au front‑end propre à Rust, mais à LLVM, ce « back‑end lourd ». La question qui se pose alors : est‑il possible de s’en débarrasser ? La réponse est oui, miaou. Il existe un back‑end appelé Cranelift, créé spécifiquement pour ce besoin. Il a d’abord été connu sous le nom de Cretonne, lancé en 2016, développé par la Bytecode Alliance. Il a été initialement conçu comme le back‑end de génération de code du paquet Wasmtime ; plus tard, l’équipe Rust l’a adopté comme back‑end de génération de code optionnel.

Alors, où Cranelift est-il censé être rapide ? La meilleure phase est pendant le développement. À ce stade, vous n'avez peut-être absolument pas besoin d'un code machine quasi parfait et extrêmement optimisé pour l'efficacité d'exécution ; peut-être avons-nous simplement besoin d'un retour rapide, tant que ce code machine pas si formidable est sémantiquement équivalent au code machine proprement généré par LLVM. À ses débuts, Cranelift n'y arrivait pas vraiment en raison de nombreux Edge Cases. Même s'il y en a encore aujourd'hui, la situation s'est nettement améliorée : la probabilité que Cranelift fonctionne alors qu'LLVM échoue est désormais à peu près la même que celle de déclencher un rustc ICE en écrivant du Rust. Pour générer un code machine extrêmement optimisé, LLVM exécute des dizaines de passes d'optimisation, comme le déroulage de boucles, la vectorisation, la propagation de constantes, l'élimination de code mort, et bien d'autres encore... énormément d'étapes polies couche après couche. La philosophie de conception de Cranelift est totalement différente : il réduit considérablement ces étapes d'optimisation pour ne conserver que l'allocation de registres et la sélection d'instructions les plus basiques, accomplissant la génération de code en un seul balayage linéaire, sans itérations répétées. De plus, sa conception IR est plus légère, spécifiquement pensée pour traduire rapidement l'IR de niveau supérieur en code machine, contrairement à LLVM IR qui traîne des décennies de fardeau de généralisation.
Une fois les avantages passés en revue, quel en est le coût ? L'inconvénient le plus évident est que le code généré par Cranelift offre des performances à l'exécution inférieures à celles de LLVM, étant environ 10 % à 30 % plus lent selon le scénario. Mais en y réfléchissant bien, on réalise que cela n'a aucune espèce d'importance en phase de développement. Pourquoi avoir besoin d'autant de rapidité ? Êtes-vous en train de faire du benchmark ou un CI release ? Tout ce que je veux, bordel, c'est que cargo build s'exécute plus vite pour voir si la logique fonctionne. De plus, je parie que la plupart des programmes que vous écrivez n'occupent pas plus de 80 % des ressources de calcul la majeure partie du temps ; il faut bien qu'il y ait du trafic métier qui arrive, et quelle peut bien être la taille de ce trafic pendant le dev ? Le CPU passe probablement son temps à se tourner les pouces ! Tout ce qui m'importe, c'est de voir le résultat et de vérifier la logique. Alors, troquer du temps de compilation contre de l'efficacité à l'exécution, ne devrait-on pas inverser le calcul dans la boucle de développement ?
Unités de génération de code
Parlons également de codegen-units. Il s’agit en fait d’un paramètre qui contrôle le nombre d’unités les plus petites dans lesquelles le compilateur découpe un crate pour que le backend les traite en parallèle. Par défaut, en mode debug, il y en a 256, et en mode release, 16. Plus le nombre est grand, plus le degré de parallélisme est élevé et plus la compilation est rapide — car plusieurs cœurs CPU peuvent être exploités simultanément pour la génération de code du backend. Le coût est une optimisation moindre, car chaque unité est optimisée indépendamment ; LLVM (ou Cranelift) voit un contexte plus restreint, ce qui réduit les opportunités d’inlining et d’optimisation entre les unités.
Cependant, dans une vraie version Release, il faut généralement utiliser codegen-units = 1, un paramètre extrême ; ce n’est qu’ainsi que le produit pourra être optimisé au maximum. Après tout, vous utilisez Rust : échanger le temps de compilation contre des performances d’exécution est tout à fait normal.
Niveau d'optimisation
Une autre configuration souvent négligée mais qui peut être ajustée est opt-level. Par défaut, elle devrait être sur "3", mais pour les builds Release, la pratique la plus courante est de la régler sur "z". C'est une optimisation gratuite de la taille de l'artefact sans modifier le code, alors pourquoi s'en priver ? Cependant, en mode développement, elle doit être configurée sur "0", car c'est en ne réalisant aucune optimisation que l'on obtient la vitesse la plus rapide.
En outre, il existe une petite astuce : Cargo.toml peut en fait définir des niveaux d'optimisation différents pour les dépendances et votre propre code.
[profile.dev.package."*"]
opt-level = 3Cela permet de compiler les dépendances tierces avec O3, ce qui n'augmente que le temps de la première compilation à froid, les suivantes étant incrémentales. Les dépendances externes n'étant généralement pas mises à jour fréquemment, utiliser O3 pour celles-ci et O0 pour votre propre code métier offre une bonne expérience en mode dev en équilibrant vitesse et taille dans les cas généraux. Cependant, en mode Release, je recommande toujours de pousser O3 ou Z au maximum, sans hésitation.
Optimisation à l'édition des liens
LTO (Link‑Time Optimization) est une autre fonctionnalité qui consomme énormément de mémoire et de performances de compilation, et la plupart des projets recommandent de l’activer pour les builds Release. En compilation normale, chaque crate est optimisé indépendamment, le backend du compilateur ne voit pas les relations d’appel entre les crates, ce qui empêche certains inlinings inter‑crate et l’élimination du code mort – un scénario très fréquent. LTO brise cette limite en donnant à l’optimiseur, lors de l’édition de liens, l’IR de tous les crates afin d’effectuer une optimisation globale. Mais ne placez jamais lto = true de façon irréfléchie dans le profil, cela ralentira votre compilation Rust au point de douter de votre existence ; ajoutez‑le simplement lorsque vous utilisez le profil Release.
Supprimer les symboles de débogage
Comme pour le reste, la debuginfo et les symbols de Rust se trouvent ici. Activer strip en mode release permet de réduire considérablement la taille – une différence hallucinante du style (souvent 50MB vs 5MB). Supprimer les symbols est également plus sûr ; après tout, la plupart des fuites de source code frontend surviennent quand des maps sont accidentellement push sur npm :(
Donc, sous le profil dev, il n’y a pas grand-chose à dire ; on ne peut pas simplement strip les debuginfo, sinon gdb / lldb ne pourront plus être débogués normalement et la backtrace ne contiendra aucun nom de fonction utile. Notez cependant que lors de l’empaquetage sous ArchLinux, le strip est appliqué par défaut, ce qui crée un léger frottement avec nous puisqu’on l’a déjà stripé, ce qui peut entraîner des erreurs ultérieures. Lors de la rédaction d’un paquet AUR, pensez à sauter explicitement cette étape.
Renoncer à panic!
Dans la logique métier normale, panic ne devrait pas exister. Lorsque du code métier classique rencontre une erreur au runtime, tout Err conçu pour la tolérance aux pannes doit être retourné en toute sécurité plutôt que d'effectuer un panic brutal. Tout comme un ErrorBoundary React, cela doit être traité normalement comme une erreur ; un panic ne devrait se déclencher que si le code a la certitude d'être entré dans un état impossible et irrécupérable. Ma philosophie personnelle est d'abandonner le panic. Pourquoi ? Parce qu'en pratique, le code métier peut être divisé en 3 niveaux de tests : le premier niveau est le happy path, le deuxième niveau correspond aux chemins d'erreur (une possibilité parmi une infinité), et le troisième niveau seulement concerne les Edge Cases extraits de manière exhaustive par du fuzzing ou des tests de collision. D'un point de vue ingénierie, le chemin nominal peut être testé à 100 %, et pour les chemins d'erreur, on peut choisir 1 variante parmi n dans une catégorie donnée. Les tests de collision sont purement une question de temps et n'en valent la plupart du temps pas la peine ; lorsque votre volume d'utilisateurs augmentera vraiment, vous saurez vous-même quand il faudra le faire. En général, je ne le fais jamais.
Pour un code de bonne qualité, il faut couvrir un test de chemin heureux et au moins un test de chemin d’erreur. Avec cette unique route d’erreur, il suffit de tester le cas où un Err est renvoyé, d’écrire une logique de secours, et ainsi on peut naturellement thiserror énumérer ou anyhow intercepter et aplatir les enregistrements de journal. En résumé, votre code vous oblige à mettre en place des mécanismes pour gérer ces situations ; si vous avez atteint ce niveau, panic devient inutile pour vous – on peut le voir comme « théoriquement impossible », mais cela reste théorique. Dans le monde réel, il existe des erreurs d’OS, des erreurs mémoire, des inversions de bits dues aux rayons cosmiques, des dépassements étranges… Vous ne pourrez jamais couvrir tous ces cas. Alors pourquoi éviter le panic ? Parce que ces situations sont pour la plupart hors de votre responsabilité – environ 90 % ne sont pas causées par votre code. L’essence de panic est que, lorsqu’il survient, Rust remonte la pile d’appels, appelant chaque destructeur (drop) de chaque cadre pour libérer les ressources. Ce processus nécessite que le compilateur génère des tables supplémentaires unwind, qui vous aident à repérer les erreurs logiques de votre logique métier. Si les erreurs sont majoritairement externes, cette information a peu de valeur, elle gonfle aussi la taille du binaire et alourdit le travail du linker – la désactiver est la bonne solution. Une fois que votre code est bien testé et que vous avez confiance, vous pouvez configurer panic = abort.
Benchmark réel
Tout ce discours est inutile ; voyons réellement les améliorations obtenues grâce à ces combinaisons. Ici, je prends un petit projet ludique que j’ai réalisé il y a quelques mois comme référence.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Language Files Lines Code Comments Blanks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CSS 5 152 113 16 23
Go 77 14556 12409 584 1563
JavaScript 23 2506 1999 232 275
JSON 93 1780 1780 0 0
Just 1 408 275 71 62
Makefile 1 15 8 3 4
Shell 16 1440 1075 166 199
SVG 1 9 9 0 0
TOML 20 502 419 3 80
TSX 75 3257 2718 132 407
TypeScript 390 36643 30350 1627 4666
─────────────────────────────────────────────────────────────────────────────────
HTML 2 41 32 7 2
|- CSS 1 37 37 0 0
(Total) 78 69 7 2
─────────────────────────────────────────────────────────────────────────────────
Markdown 95 5344 0 3578 1766
|- BASH 3 8 8 0 0
|- Go 2 21 21 0 0
|- HTML 1 25 15 10 0
|- JSON 6 317 317 0 0
|- Rust 1 10 9 0 1
|- TSX 1 11 9 0 2
(Total) 5736 379 3588 1769
─────────────────────────────────────────────────────────────────────────────────
Rust 181 31654 26951 1032 3671
|- Markdown 86 581 0 564 17
(Total) 32235 26951 1596 3688
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total 980 99317 78554 8025 12738
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Dans ce projet, la partie Rust représente environ 32 K SCoL Rust, hors taille des dépendances.
Ce dépôt est en réalité un monorepo contenant de nombreux sous‑projets, mais on peut y identifier deux exemples typiques. Tout d’abord, examinons le paquet Skeleton : ses caractéristiques sont évidentes – un volume de code important avec très peu de dépendances, ce qui en fait la plus grande part de codegen du projet entier, le rendant idéal pour Cranelift. De plus, ce paquet est très « clean », le terme « clean » signifiant ici du Rust purement Safe.
En revanche, le paquet CLI ci-dessous n'est pas très approprié. Avoir de nombreuses dépendances n'est pas vraiment un problème — en théorie, cela reflète mieux l'efficacité —, mais il y a ici un choix cornélien classique. Vous pouvez voir dans le coin supérieur gauche que la dépendance ring est marquée comme optionnelle ; c'est parce que le projet utilisait à l'origine aws-lc-rs, ces deux éléments étant des Backends d'algorithmes de chiffrement en Rust. Mais pourquoi la rendre optionnelle ? C'est parce que ring est écrit en pure Rust, tandis que l'autre est intégré via FFI en assembleur Asm, spécifiquement conçu pour l'accélération assembleur sur les architectures cpu courantes telles que x86 et arm64. Cependant, c'est aussi là que le bât blesse, car la magie de Cranelift est strictement limitée au pure Rust. Dès que vous introduisez du Unsafe Code, ou du FFI C, ASM, etc., la compatibilité de Cranelift devient en réalité extrêmement mauvaise.
Cependant, ce n’est pas insoluble ; on peut effectivement sélectionner le backend avec cfg et, en mode Cranelift, passer ring au compilateur. Après avoir configuré les profils dev et release comme indiqué ci‑dessus, il suffit d’exécuter une comparaison de compilation via la CLI. Dans ce cas, le profil de développement est au moins trois fois plus rapide que le profil de publication, même en compilation à froid sans incrémentalité ; et en utilisant O0 et en désactivant LTO, le profil dev devrait être des dizaines de fois plus rapide que le release lors des mises à jour incrémentielles ultérieures. En effet, la « magie » du type LTO consiste essentiellement à aplatir les frontières entre les crates ; une fois que tout se fusionne en une seule unité, modifier une ligne de code oblige à recompiler tout le projet, empêchant ainsi une mise à jour incrémentielle correcte.
![Ceci est une capture d'écran d'une fenêtre de terminal intitulée "~/C/P/seam" montrant la partie finale d'une exécution de `cargo build --release` pour un projet appelé "seam". Le déroulement répertorie des dizaines de crates de dépendances compilées en séquence — walkdir, rand, tungstenite, sha2, toml, notify, rquickjs, clap et d'autres — suivi des membres propres de l'espace de travail du projet tels que seam-codegen, seam-skeleton, seam-injector-wasm, seam-engine-wasm, seam-server, seam-server-axum, seam-cli, et plusieurs backends d'exemple (demo-server-rust, github-dashboard-axum, markdown-demo-rust, i18n-demo-axum). Les lignes finales confirment le succès : "Finished `release` profile [optimized] target(s) in 1m 05s," suivi d'une invite de shell inactive affichant `canmi@xyy ~/C/P/seam (main)>`. Cela constitue la preuve d'une construction release propre et complète d'un monorepo Rust multi‑crate avec des composants WASM et serveur Axum.](https://cdn.ffoni.com/image/e8b530aa153a78f1cdea3d2980f80ecd.jpeg)
![Une capture d’écran du terminal montre la fin d’un journal de construction Cargo pour un espace de travail Rust nommé « seam », intitulé `~/C/P/seam` avec le shell exécuté sous l’utilisateur `canmi` sur la branche `main`. Des dizaines de crates de dépendances se compilent successivement (console, reqwest, clap, tokio-tungstenite, notify, indicatif, et d’autres), suivies des crates du projet — seam-codegen, seam-server-axum, seam-skeleton, seam-injector-wasm, seam-engine-wasm, seam-cli, toutes en version 0.5.38 — et plusieurs back‑ends d’exemple (github-dashboard-axum, i18n-demo-axum, markdown-demo-rust, demo-server-rust). Le journal se termine par `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 19.23s`, indiquant une construction de débogage réussie de l’ensemble de l’espace de travail, y compris ses applications d’exemple.](https://cdn.ffoni.com/image/71aa2631cd4c616d2ecca6e510cf06f4.jpeg)
En examinant les résultats globaux, ces deux éléments ne sont pas vraiment si différents ; ce n'est pas une révolution, mais la différence est absolument perceptible. La raison principale est qu'Apple Silicon est tout simplement ultra-puissant : les performances mono-cœur et la bande passante mémoire des puces de la série M sont incroyablement élevées, et des tâches lourdes en calcul et en E/S comme la compilation tirent pleinement parti de ces atouts. En général, les faire tourner sur Linux marquerait un écart encore plus grand, mais si l'on installe Asahi Linux ou un système similaire sur un MacBook, c'est sans aucun doute Linux qui l'emporte.
Éditeur de liens obsolète
Sous Linux, Rust utilise par défaut GNU ld (bfd) — oui, exactement le plus ancien et le plus lent. Étant mono-threadé et traitant la résolution de symboles ainsi que la réallocation par balayage sériel, il ralentit nettement les projets lorsqu'ils deviennent volumineux. Sous Linux, vous pouvez essayer mold, écrit par @Rui Ueyama : la configuration est très simple, il suffit d'ajouter une ligne dans .cargo/config.toml.
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
L'amélioration reste très nette, mais il est regrettable que sur macOS, il n'y ait que sold, un projet commercial. La bonne nouvelle, en revanche, c'est que lld sur macOS ou le ld64 fourni par Apple n'est déjà plus lent en soi, surtout le nouvel éditeur de liens depuis Xcode 15 dont le gain de vitesse est extrêmement visible. Le seul inconvénient de ce truc, c'est que sur un CI macOS non supervisé et après une mise à jour, il faut à chaque fois sudo Accept une clause, ce qui a fait planter mes tâches planifiées du CI à plusieurs reprises.
MUSL
Je suis moi-même un grand fan de MUSL et je déteste vraiment GNU glibc, mais pour le développement, je vous recommande tout de même d'utiliser x86_64-unknown-linux-gnu. Pourquoi ? Parce que c'est rapide. Le plus grand atout de MUSL est de pouvoir générer des exécutables entièrement liés statiquement — sans dépendre d'aucune bibliothèque dynamique sur le système, ce qui est compilé peut être copié sur n'importe quelle machine Linux et s'exécuter directement, ce qui convient particulièrement aux conteneurs et à l'embarqué. Mais sa lenteur provient également du liage statique : comme l'éditeur de liens doit tout empaqueter, la charge de travail durant la phase de liage est bien plus importante qu'avec le liage dynamique.
Cependant, la bonne nouvelle est que si vous utilisez macOS, vous n'avez pas du tout à vous inquiéter : aarch64-apple-darwin est actuellement le seul choix possible. Par conception, macOS ne prend pas en charge la liaison entièrement statique, ce qui est en fait une bonne chose du point de vue de la vitesse de compilation. Ainsi, le dev profile utilise gnulibc sur Linux et libSystem sur macOS, tandis que le release profile peut utiliser musl à la fois sur macOS et Linux. En raison de problèmes liés aux choix par défaut de linker et ar sur macOS, le seul point à noter est que vous devrez peut-être configurer .cargo/config.toml.
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
ar = "x86_64-linux-musl-ar"Au fait, n’utilisez surtout pas zig ; le zbuild de nightly Rust pose actuellement de gros problèmes. Si vous avez besoin de compilation croisée et que l’hôte est Linux x86, nous recommandons cross. Le faire avec Docker fournit un environnement complet et très pratique. Si vous préférez glibc, cela fonctionne également très bien, car glibc ne garantit que la rétro‑compatibilité. En pratique, on compile avec la version de glibc d’une version majeure de Debian (par ex. Debian‑2). Sinon, une glibc trop récente rendra de nombreuses machines inopérantes ; ce n’est pas à cause d’une nouvelle fonctionnalité, mais simplement parce que le simple contrôle de la chaîne de version vous agace. Si l’hôte est un mac, utilisez rustup natif target + cargo build --release .
UPX est une mauvaise chose
Un autre point qui mérite d'être discuté est l'utilisation de UPX ; je sais qu'il paraît plutôt contradictoire et peu logique de dire que j'aime musl tout en cherchant à obtenir une faible taille...
ooooo ooo ooooooooo. ooooooo ooooo
`888' `8' `888 `Y88. `8888 d8'
888 8 888 .d88' Y888..8P
888 8 888ooo88P' `8888'
888 8 888 .8PY888.
`88. .8' 888 d8' `888b
`YbodP' o888o o888o o88888o
The Ultimate Packer for eXecutables
Copyright (c) 1996-2026 Markus Oberhumer, Laszlo Molnar & John Reiser
https://upx.github.ioMais en pratique, l'existence d'UPX permet de résoudre ce problème. Le principe d'UPX consiste en fait à compresser votre BIN et à l'envelopper dans un stub de décompression, pour le décompresser en mémoire au moment de l'exécution avant de l'exécuter. Cela permet de réduire la taille du binaire à environ 30 % à 50 % de sa taille d'origine, rendant l'avantage en termes de taille très net. Mais c'est précisément pour cette raison que l'association UPX + liaison dynamique GNU glibc pose de gros problèmes : les binaires glibc liés dynamiquement contiennent des sections spéciales et des mécanismes de chargement dynamique que la compression UPX risque d'endommager, ce qui fait que les bibliothèques dynamiques sont introuvables à l'exécution segment fault ; en revanche, musl est parfaitement adapté à UPX. Il est entièrement statique par nature et stable en interne, de sorte que la décompression et la compression après UPX se font très proprement. Ajouter une étape UPX dans le CI de release pour musl serait donc un véritable plus.
De plus, il ne faut pas abuser de UPX. UPX ne convient qu'aux tâches de longue durée ou à faible fréquence. Pour les tâches de longue durée, cela s'explique par le fait que UPX doit décompresser une fois en mémoire au démarrage. Même si ce processus est entièrement automatique, il nécessite un petit temps de démarrage à froid, ce qui a peu d'impact en général mais s'avère fatal sur des chemins à haute fréquence. Ainsi, lancer une tâche de longue durée une seule fois pour la laisser tourner en arrière-plan convient très bien. Cependant, je ne recommande pas de fourrer aveuglément du UPX dans des images Docker pour de tels services durables, car il s'agit en réalité d'échanger de la mémoire contre de l'espace disque, ce qui est une très mauvaise affaire. De plus, la compression des Layers de conteneurs fait exactement la même chose que UPX pour réduire la taille de téléchargement lors de la distribution. L'environnement recommandé concerne plutôt certains usages CLI : cela permet d'économiser une quantité énorme d'espace disque avec un temps de décompression au démarrage presque imperceptible. De plus, musl est naturellement adapté aux outils CLI. Sinon, je devrais pointer du doigt les assistants AUR : ce truc écrit en Go sans liaison statique qui a fait que yay s'est cassé sur mon ArchLinux à cause de bibliothèques dynamiques introuvables. Comme il s'agit lui-même du gestionnaire de paquets système, il n'y avait pas d'autre solution que de démarrer sur un LiveISO et de faire un chroot pour sauver le système.
Résumé
Même si ces configurations pratiques permettent de différencier rapidement les profils dev et release, leurs avantages deviennent de plus en plus évidents à mesure que le code du projet s’étend. Cela dit, je recommande toujours d’ajouter un job CI pour exécuter le Rust Stable LLVM Build, idéalement en l’associant à l’ensemble de vos tests. Ainsi, vous n’aurez plus besoin de basculer vers un Rust nightly en local, les Edge Cases ne vous piègeront pas et tout problème sera intercepté par la CI lors du push. C’est actuellement le mode DX le plus confortable ; c’est tout pour le moment, je reviendrai dessus la prochaine fois que j’aurai du temps.