Blog des Gens Compliqués

Comment rendre un exécutable écrit en Rust plus léger

31/05/2026 15:48:41+02:00|Par DkVZ
Informatique & Web
18 minutes de lecture (facile)

Table des matières

Introduction

Hey ça faisait longtemps que j'avais plus parlé de Rust.

Vous avez peut-être remarqué que les binaires compilés en Rust, en plus d'avoir un temps de compilation un peu élevé, ils pèsent plusieurs MEGABYTES, parfois juste pour un Hello World.

Le dossier target du projet de base Hello World compilé affichant 436Kb pour le binaire sous Linux
Bon OK c'est moins d'un MEGABYTE mais ça reste beaucoup pour trois lignes de code

Excusez-moi monsieur, 436K c'est beaucoup?

Pas depuis que le monde a été déformé par l'inefficacité péremptoire et générale du développement logiciel moderne.

OK non en fait c'est vraiment pas beaucoup. Me semble que c'était pire avant.

Mais c'est pas grave, on veut MOINS.

Par exemple, un Hello World en zig compilé avec l'option "compacte":

zig build-exe -O ReleaseSmall main.zig

Et on se retrouve avec à peu près 10 fois moins de données:

Taille d'un Hello World en Zig, seulement 4.2Kb
Quand on y pense, l'équivalent de plus de 4000 caractères textuels juste pour dire bonjour ça reste beaucoup

Tout ça s'empire avec des ajouts de librairies externes et on arrive vite à plus de 10MB pour un vieux serveur HTTP.

Attends, je viens de télécharger VS Code et il fait 300MB

Ben ouais. C'est bien ce que je disais.

Tavu combien ça coûte un bête disque-dur-qui-tourne depuis qu'Open AI a acheté tout le matos informatique du monde entier à l'avance pour plusieures années?

Comment réduire la taille de mon exécutable?

T'es pas le premier à te poser cette question. Enfin, tu dois être genre le deuxième après moi. Mais c'est pas grave, je te comprends.

Un jour j'ai découvert un utilitaire nommé strip au sujet duquel aucune blague n'est possible.

Je pense qu'il fait partie de GCC ou d'un paquet générique de compilation de bidules.

Le rôle de strip est pas super clair. Il enlève des symboles et euh... Des autres données??

Haut de la page de manuel de strip qui dit qu'il... Enlève des symboles et des autres données
Voilà qui a l'air prometteur (??)

Par exemple, si je prends mon projet obscur de génération d'extraits de code pour vim (oui c'est le pire exemple que pouvais prendre mais je l'avais sous la main):

Taille non-écrémée Taille après strip
1.1 MB855 KB

Je vous parlerai un jour de ce projet même si ça n'intéresse personne, c'est un peu le thème de ce blog. Mais là, concentrons nous sur l'effet du strip.

On constate une différence d'environ 250 KB soit tout de même autour de 23% de la taille de départ.

C'est quoi ces symboles mystérieux?

Je sais qu'en invoquant un autre utilitaire de la même famille, objdump, on peut lui demander de balancer tous les "symboles" (je pense on saura jamais ce que c'est exactement):

objdump -t "NOM_DE_L_EXECUTABLE"

Et il m'indique bien qu'il n'y a [plus] rien:

la sortie de la commande précédente indique "no symbols"
Ah ouais, y en a plus des symboles

Un autre utilitaire "célèbre" parmi les binutils s'appelle strings .

Sa finalité est de parcourir un fichier binaire et chercher les séquences de chiffres binaires qui peuvent représenter des caractères consécutifs, avec un minimum de caractères texte à détecter (4 par défaut, je crois) avant de tomber sur un caractère "non texte".

Ce bête programme est connu en sécurité parce qu'il permet d'obtenir un niveau basique d'informations sur un programme avec des traces de qui l'a compilé et comment, ainsi que tout le texte qui est dans le programme.

Si c'est un jeu vidéo narratif, par exemple, vous pouvez retrouver tous les dialogues du jeu avec la commande strings (pouvu qu'ils aient tout bourré dans l'exécutable et n'utilisent pas des fichiers de ressource séparé, ce qui est somme toute plus propre).

Si je strings un bidule que je viens de compiler en Rust, je constate qu'il révèle mon répertoire home sur tout un tas de lignes, avec mon presque vrai nom et tout:

Tout un tas de mention à /home/william dans ma sortie de strings
C'est qui ce William

Bon ben raison de plus de travailler avec du CI/CD. L'OS utilisé pour compiler apparait aussi dans les premières lignes.

D'après ce que je comprends, les fameux SYMBOLES ressemblent à ceci (aussi visible dans la sortie de strings):

affiche des caractères bizarres avec des messages d'erreur et ce ressemble à de l'info de déboguage
Je pense que ça sert au déboguage

Si j'en reviens à mon projet mentionné plus haut, je vois que la sortie de strings représente une bonne partie de ce qui est éliminé du binaire par strip mais pas tout.

Ceci dit, gagner 20% de taille moi je prends.

Le cas Microsoft Edit

Vous avez déjà entendu parler de MSDOS?

Ce système d'exploitation légendaire en LIGNE DE COMMANDE possède bien évidemment un éditeur de texte (aussi en ligne de commande bien entendu): edit.exe.

J'en ai déjà parlé quelques fois, je l'utilise aussi dans cette vidéo mémorable.

Un fichier HTML ouvert dans edit (présent dans la vidéo citée plus haut)
Désolé pour la qualité &mash; C'est pour le côté authentique

Et surtout je parle de QBasic dans plusieurs articles également et les deux sont historiquement liés parce que, edit.com (.com étant un type de fichiers et pas un morceau de nom de domaine) appelait historiquement QBasic pour le transformer en éditeur de texte sans les options de programmation en BASIC.

Ce qui est somme toute assez malin et efficace en terme de place. Et ouais, c'était mieux avant.

Capture d'écran de QBasic, editeur pour programmer en language BASIC
De mon temps, toute ta vie c'était un écran bleu

L'éditeur autorisait l'usage de la souris qui déplaçait un gros curseur rectangulaire. Un peu comme le curseur de vim en fait.

Attendez une minute... Pourquoi j'ai pas encore parlé de vim dans cet article?

Ben justement, on pouvait un peu tout faire au clavier avec edit en utilisant la touche Alt+<LETTRE> pour invoquer un menu, puis presser la touche de raccourci de l'élément (qui est soulignée).

En fait il est toujours vaguement possible d'appeler les menus des programmes Windows comme cela mais ça s'est perdu parce que les gens sont devenus fades en complémentant leur absence d'envie d'investir dans l'apprentissage d'automatismes qui pourraient augmenter leur productivité dans l'avenir plutôt que de juste cliquer sur des machins tout de suite.

Meme du type qui a les veines qui sortent du front et qui en peut plus dans une salle de classe
Moi qui me retiens de parler de vim

Je sais pas si certains d'entre-vous connaissent l'éditeur en ligne de commande nano, assez populaire sur Linux dans la famille Debian.

Personne? D'accord. Ben je trouve que edit est mieux que nano. Tant qu'à utiliser un éditeur de prout autant y aller avec la souris et tout.

Compare la taille de nano et edit en ligne de commande, de 294Kb contre 295Kb respectivement
Ah ben, en plus, nano est plus grand de 1Kb

Mais attends, comment on pourrait remplacer nano par edit? C'est un vieil éditeur pour MS DOS.

He bien figurez-vous que chez Microsoft, ils ont décidé que ce serait une bonne idée de mettre des développeurs qui coûtent bien cher dans leur minuscule appart de Californie à 3000€/mois pour développer une NOUVELLE version MODERNE de edit.

VASY je veux cet éditeur! Il me manquait que ça pour passer ma vie en ligne de commande

L'interface originale a été vaguement recréée, en moins bleu et respectant les couleurs annoncées par l'émulateur de terminal (rendons grâce à dieu, lequel que vous voulez).

Etant donné tous leurs problèmes de sécurité liés à l'usage de C/C++, Microsoft est devenu fan du langage Rust qui sera dès lors utilisé pour écrire ce programme.

Ils publient des binaires précompilés pour linux sur leur page Github releases.

Si vous êtes sous Windows, edit est normalement déjà présent à moins que vous ayez zappé une cinquentaine de mises à jour. Je vous en voudrai pas si c'est le cas.

Notez que pour Linux il s'agit d'installer un paquet supplémentaire générallement dénommé libicu-dev. Sans lui, la recherche ne fonctionne pas du tout.

Et alors, de plus, j'avais déjà un programme "edit" dans mon PATH faisant partie d'utilitaires obscurs mailcap que j'ai désinstallés.

Page de manuel de l'utilitaire edit de la suite d'utilitaires mailcap qui semble lié à la lecture ou envoi d'email en ligne de commande
Aucune idée de ce que c'est que ce truc mais j'ai pu le désinstaller sans devoir désinstaller Gnome donc c'est bon signe

Le plus simple pour ensuite installer edit sous Linux ou Mac consiste à chopper la bonne archive depuis leur page de releases qui contient normalement un bon vieux mono-fichier "edit" (si seulement tout pouvait être si simple) à bourrer quelque part dans le PATH.

Vous êtes maintenant prêts à éditer du texte depuis la ligne de commande. Une compétence qui deviendra très utile quand le seul ordinateur que l'on pourra acheter sera un vieux serveur Linux à louer quelque part dans le CLOUD.

Deux terminaux côté à côté avec edit à gauche et une coloration syntaxique du HTML du texte de ce même article
Quoi? On a de la coloration syntaxique pour autre chose que le BASIC?

Comme mentionné plus haut, le programme utilise les couleurs définies par le terminal, comme on peut le voir sous Windows avec un Windows Terminal à droite et Wezterm à gauche, les deux ayant un thème de couleurs différent.

Deux fenêtres de terminal, celle de gauche a un thème verdâtre et celle de droite est le Windows Terminal tout noir par défaut
Je garantis pas que ça donne bien avec tous les thèmes

Bon, après, je dois tout de même avouer qu'il se souviens pas de mes préférences (j'ai besoin de Word Wrap ou je deviens fou, je suis d'ailleurs le seul à l'utiliser sur vim) et c'est pas clair ce qu'on peut faire avec l'hypothétique fichier de préférences JSON.

Je peux savoir pourquoi on parle d'un éditeur de texte depuis une heure?

Figurez-vous que les auteurs du nouveau edit sont quelque peu obsédés par la taille de leur truc. Encore plus que moi.

Sous Windows le binaire pèse un peu de plus de 200Kb, et je pense que la coloration syntaxique prend un bon pourcentage:

Page de propriétés de edit.exe dans C:\Windows\system32 affichant 260Kb de taille de fichier
Les filous l'ont bourré dans System32 avec le reste du grenier de mamy

Je rappelle que, pour VS Code, l'installateur (compressé) prend 170MB.

L'installateur de VS Code annonce qu'il a besoin de 766MB d'espace disque
Ben ouais, le CSS ça se compresse bien

On va discuter de toutes les optimisations de taille réalisées pour edit avant de les voler allègrement comme si on était une startup d'IA un lundi matin.

Options de compilation à utiliser

Ne me dis pas qu'on arrive enfin au vif du sujet!

La majorité des options se trouvent dans Cargo.toml, avec un petit supplément dans le répertoire .cargo qui devrait probablement un jour être abandonné.

Fût un temps où ils recommendaient chaudement de compiler edit avec la version nightly (potentiellement pas stabe) de Rust mais ils semblent tenter d'en revenir.

Voici les options du profil de compilation release qu'ils utilisent:

# We use `opt-level = "s"` as it significantly reduces binary size.
# We could then use the `#[optimize(speed)]` attribute for spot optimizations.
# Unfortunately, that attribute currently doesn't work on intrinsics such as memset.
[profile.release]
codegen-units = 1           # reduces binary size by ~2%
debug = "full"              # No one needs an undebuggable release binary
lto = true                  # reduces binary size by ~14%
opt-level = "s"             # reduces binary size by ~25%
panic = "abort"             # reduces binary size by ~50% in combination with -Zbuild-std-features=panic_immediate_abort
split-debuginfo = "packed"  # generates a separate *.dwp/*.dSYM so the binary can get stripped
strip = "symbols"           # See split-debuginfo - allows us to drop the size by ~65%
incremental = true          # Improves re-compile times

Tout cela s'applique automatiquement si on build avec l'option --release, par exemple:

cargo build --release

Il y a d'autres profils de compilation Rust comme comme dev, test (hérite de dev par défaut) et bench (hérite de release par défaut).

Détaillons tout cela avant que je vous livre ce que j'utilise personnellement.

codegen-units

Censé contrôler à quel point LLVM (la sous couche de compilation statique ou dynamique) peut découper le code en plusieurs morceaux pour les compiler en parallèle.

La valeur par défaut est plus élevée que 1. L'idée étant que si on passe sur 1, la compilation est plus lente mais on est certain d'avoir le binaire le plus optimisé possible.

Je pense que ça change pas grand chose, tant en taille qu'en temps de compilation mais si vous payez un worker de CI/CD à la seconde et que quelqu'un provoque 15000 recompilations automatiques ça a peut-être de l'importance.

debug

La valeur par défaut est en fait none pour le profil release et devrait réduire la taille davantage que full.

Je retire totalement cette option pour utiliser la valeur par défaut.

Sur au moins un de mes projets, ça ne change quasi rien à la taille du binaire alors que dans un autre ça la multiplie littéralement par 5.

Aussi, je sais pas ce que ça veut dire qu'on peut "pas le debug" j'imagine qu'on peut quand même l'ouvrir dans Ghidra.

lto

Le passer à true augmente le temps de compilation mais optimise le code au maximum en analysant l'entièreté du projet.

J'imagine que l'impact de cette option augmente avec la taille du code.

Ils disent dans la doc de Rust que LTO n'est pas du tout utilisé si codegen-units vaut 1. Je sais pas trop qui a raison entre la doc Rust et Microsoft mais d'après mes expériences, ça ne fait pas de mal d'avoir les deux options et gratte effectivement quelques KB.

opt-level

Censé contrôler le niveau d'optimisation dans l'idée qu'augmenter cette valeur numérique opt-level est mieux pour le résultat final mais augmente le temps de compilation. Un peu comme pas mal d'autres options vues précédemment, quoi.

Lui filer la valeur s (qui n'est pas numérique, ouaip) est censé donner le binaire le plus petit possible en générant moins de code dupliqué en ligne.

Pour un peu expliquer l'affaire: les appels de fonction existent vraiment en langage machine bien qu'on les appelle généralement procédures et consistent à sauter à un autre endroit du programme (qui est une suite d'instructions se suivant de haut en bas) en empilant l'adresse d'où on a sauté vers la procédure, qui possède alors sa propre pile qui sera nettoyée à la fin de la dite procédure, avant de récupérer l'emplacement précédent et y retourner.

Comme la lourdeur de la phrase précédente implique, tout ça prend du temps.

S'il s'agit d'une fonction qui ne réalise que quelques actions, les compilateurs ont tendance à ne pas créer de procédure dans l'exécutable et juste copier-coller tout le code de la fonction à son emplacement d'appel dans le programme, potentiellement plusieurs fois. Ce qui augmente la taille du binaire.

En gros, l'appel de procédure a totalement disparu, remplacé par le code lui-même. Ce processus s'appelle inlining ou un truc du genre.

Il est particulièrement important pour les performances parce que beaucoup de langages (dont Rust) utilisent une myriade de petites fonction en interne et dans la librairie de base.

Je préfère laisser cette option sur sa valeur par défaut. Notamment parce que son résultat est assez volatile selon le projet et je pense que, quand on choisit de s'autoflageller en bossant avec Rust, c'est pour obtenir un maximum de performances même si ça augmente la taille de quelques Kb.

panic

Il s'agit sans doute de l'option la plus intéressante:

panic = "abort"

Elle conditionne le comportement du binaire produit en cas de "panique" — C'est-à-dire de gros problème détecté pendant l'exécution comme un dépassement de pile ou un usage de la macro panic!.

Le comportement normal consiste à balancer une VIEILLE TRACE de toute la pile.

Quelque chose de bien connu des développeurs Java (et C#) qui voient souvent de très longues stack traces.

Ce comportement permet aussi de capturer le signal de panique et l'annuler. Dé-paniquer quoi.

En plaçant la valeur sur abort on arrête le processus sans déballer des piles et des poils et ça permet de récupérer des chaines de caractères statiques qui étaient visibles avec la commande strings.

CEPENDANT, ça ne fait pas gagner 50% de taille comme annoncé en commentaire par les auteurs d'edit. Sur un projet d'exécutable Windows je vois environ 2% et environ 8% sur un projet Linux ce qui indique que, comme d'habitude, la nature du projet va influencer les résultats.

En fait, pour edit, ils utilisent normalement le build "nightly" de Rust avec un fichier de config supplémentaire pour Cargo qui est dans le répertoire .cargo sur leur repo.

Dans ce fichier on trouve cette valeur mystique pour abort:

[profile.release]
panic = "immediate-abort"

Spécifiquement pour ce projet et si nightly est installé, leur script infernal d'installation Linux d'edit crée le binaire edit comme ceci:

cargo build -p edit --release --config .cargo/release.toml

Sinon, pour un projet lambda (avec nighly installé et actif) ceci devrait fonctionner:

[profile.release]
panic = "immediate-abort"

[unstable]
panic-immediate-abort = true
build-std = ["std", "panic_abort"]

C'est considéré comme "instable" (pour l'instant). Je préfère éviter de l'utiliser et rester sur le Rust "stable" que je n'arrive déjà pas à suivre.

Vous aurez un message d'erreur en tentant de compiler avec une version "stable":

Erreur de compilation mentionnant que immediate-abort n'est pas considéré comme stable
Rust est censé être un langage robuste donc ça me choque pas

split-debuginfo

Juste un prérequis pour l'option suivante qui, en gros, va virer les symboles dont on parlait bien plus haut (comme strip).

On réfléchit pas et on ajoute ça dans le tas:

split-debuginfo = "packed"

strip

Ben ouais. On en avait déjà parlé. Permet de récupérer un max d'espace, c'est de loin l'option la plus importante, à conjuguer avec l'option précédente.

De nouveau, me demandez pas à quoi servent ces symboles qu'on enlève, c'est toujours pas très clair surtout que, dans le cas de edit, il y a l'autre option debug qui est sur "full" ce qui me fait penser que ces symboles n'ont rien à voir avec le débogage sauf que par définition si.

Pouquoi on gonfle artificiellement les exécutables Rust avec du texte qui a l'air de ne servir absolument à rien (comme mon chat)?

**Bruit du vent faisant vibrer deux lamelles de lard superposées**

Utiliser "symbols" revient au même que passer la valeur à true, on veut déshabiller un max de trucs:

strip = "symbols"

# Equivalent:
#strip = true

incremental

Si placé à true, conserve encore plus de données dans le répertoire target de sorte à bien dépasser le Gigabyte pour un petit projet.

Le but étant de re-compiler plus rapidement.

Cette option de config n'a aucune incidence sur la taille du binaire final donc je m'en balance un peu et la laisse par défaut, ce qui correspond à false pour le profil release et true pour le profil dev.

Ce que je préconise

Ben ouais je suis quand même là pour avoir un avis pseudo-arrêté sur mon blog d'opinions extrêmement courageuses (rires).

Voici un bon compromis selon moi, à poser dans votre Cargo.toml:

# Stole most of this from the Microsoft Edit repo
[profile.release]
codegen-units = 1
lto = true
panic = "abort"
split-debuginfo = "packed"
strip = "symbols"

Avec ces options pour un programme Windows à la con je passe de 1800 Kb à 371 Kb.

Avec ce genre d'économies on a même plus besoin du détroit d'Hormuz.

Windows 11 avec Windows Terminal et edit.exe affichant le Cargo.toml d'un projet
Mon nouveau WORKFLOW | AU REVOIR VIM

Au passage, j'utilise vraiment plus Notepad sous Windows maintenant (quand mon environnement de dev Neovim n'est pas installé).

Déjà, les fichiers qui se re-ouvrent tout seuls ça me hérissait les poils de tétons chaque fois mais en plus y a un bouton COPILOT et des failles de sécurité.

Au moins edit c'est juste un µ&é!mù$ d'éditeur de texte qui édite du texte. Zavez-vu cet article m'a fait grandir aussi en tant qu'utilisateur de Windows.

Commentaires

Il faut JavaScript activé pour écrire des commentaires ici

Ajouter un commentaire

Votre commentaire a été ajouté
(enfin, je pense)