Réduire la taille d'un binaire Rust
Cet article est une traduction du dépôt Github johnthagen/min-sized-rust que vous pouvez également retrouver sur adriantombu/min-sized-rust
Par défaut, Rust optimise la vitesse d'exécution, la vitesse de compilation et la facilité de débogage plutôt que la taille des binaires, car pour la grande majorité des applications, c'est l'idéal. Mais pour les situations où un développeur souhaite vraiment optimiser pour la taille du binaire, Rust fournit des mécanismes pour y parvenir.
Compiler en mode release
- Version Rust Minimale : 1.0
Par défaut, cargo build
compile le binaire Rust en mode debug. Le mode débogage désactive de nombreuses optimisations, ce qui aide les debuggers (et les IDE qui les exécutent) à fournir une meilleure expérience de débogage. Les binaires de débogage peuvent être plus lourds de 30% ou plus que les binaires de release.
Pour minimiser la taille d'un binaire, compilez en mode release ::
$ cargo build --release
Retirer les symboles du binaires avec strip
- OS: -nix
- Version Rust Minimale : 1.59
Par défaut, sous Linux et macOS, des informations sur les symboles sont incluses dans le fichier .elf
compilé. Ces informations ne sont pas nécessaires pour exécuter correctement le binaire.
Cargo peut être configuré pour strip
automatiquement les binaires. Il suffit de modifier Cargo.toml
de cette manière:
[profile.release]
strip = true # Supprime automatiquement les symboles du binaire.
Avant Rust 1.59, il faut manuellement lancer la commande strip
sur le fichier .elf
à la place:
$ strip target/release/min-sized-rust
Optimiser la taille
- Version Rust Minimale : 1.28
Par défaut, le niveau d'optimisation de Cargo est de 3
pour les versions de développement., qui optimise le binaire pour la vitesse. Pour demander à Cargo d'optimiser pour une taille binaire minimale, utilisez le niveau d'optimisation z
dans Cargo.toml
:
[profile.release]
opt-level = "z" # Optimiser la taille
Notez que dans certains cas, le niveau "s"
peut donner lieu à un binaire plus petit que le niveau "z"
, comme expliqué dans la documentation opt-level
:
Il est recommandé d'expérimenter avec différents niveaux pour trouver le bon équilibre pour votre projet. Il peut y avoir des résultats surprenants, tels que ... les niveaux
"s"
et"z"
ne sont pas nécessairement plus petits.
Activer le Link Time Optimization (LTO)
- Version Rust Minimale : 1.0
Par défault, Cargo demande aux unités de compilation d'être compilées et optimisées de manière isolée. LTO demande au linker d'otpimiser à l'étape du link. Cela permet, par exemple, de supprimer le code mort et souvent de réduire la taille des binaires.
Activer LTO dans Cargo.toml
:
[profile.release]
lto = true
Supprimer Jemalloc
- Version Rust Minimale : 1.28
- Version Rust Maximale : 1.31
A partir de Rust 1.32, jemalloc
est supprimé par défault. Si vous utilisez Rust 1.32 ou plus récent, aucune action n'est nécessaire pour réduire la taille des binaires en ce qui concerne cette fonctionnalité.
Avant Rust 1.32, pour améliorer les performances sur certaines plateformes, Rust a intégré jemalloc, un allocateur souvent plus performant que l'allocateur par défaut du système. L'intégration de jemalloc a néanmoins rajouté environ 200KB à la taille finale du binaire.
Pour supprimer jemalloc
sur Rust 1.28 - Rust 1.31, ajoutez ce code en haut de votre fichier main.rs
:
use std::alloc::System;
#[global_allocator]
static A: System = System;
Réduire l'exécution parallèle d'unités de génération de code pour améliorer l'optimisation
Par défaut, Cargo utilise 16 unités de génération en parallèle pour les versions de production. Cela améliore les temps de compilation, mais empêche certaines optimisations.
Fixez cette valeur à 1
dans Cargo.toml
pour permettre une optimisation maximale de la taille du binaire :
[profile.release]
codegen-units = 1
Interrompre en cas de panic
- Version Rust Minimale : 1.10
Note: Jusqu'à présent, les options discutées pour réduire la taille des binaires n'ont pas eu d'impact sur le comportement du programme (seulement sur sa vitesse d'exécution). Cette fonctionnalité a un impact sur le comportement.
Par défaut, lorsque Rust rencontre une situation où il doit appeler panic!()
, il déroule la pile et produit un message utile. Cela requiert néanmoins du code en plus qui augmente la taille du binaire. Il est possible de configurer rustc
pour interrompre le programme immédiatement, ce qui enlève ce besoin de code supplémentaire.
Activez cela dans Cargo.toml
:
[profile.release]
panic = "abort"
Retirer les détails d'emplacement
- Version Rust Minimale : Nightly
Par défaut, Rust inclut les informations de fichier, ligne et colonne dans panic!()
et [track_caller]
pour donner des informations utiles. Ces informations occupent de l'espace dans le fichier binaire et augmentent donc la taille des fichiers compilés.
Pour supprimer ces informations de fichier, ligne et colonne, utilisez le flag instable rustc
-Zlocation-detail
:
$ RUSTFLAGS="-Zlocation-detail=none" cargo +nightly build --release
Optimiser libstd
avec build-std
- Version Rust Minimale : Nightly
Note: Voir également Xargo, qui est le prédécesseur de
build-std
. Xargo est actuellement en mode maintenance.
Un exemple de projet est disponible de le dossier
build_std
.
Rust fournit des copies précompilée de la bibliothèque standard (libstd
) dans sa suite d'outils. Cela signifie que les développeurs n'ont pas besoin de compiler libstd
à chaque fois qu'ils créent leurs applications. libstd
est lié statiquement dans le binaire à la place.
Bien que cela soit très pratique, il y a plusieurs inconvénients si un développeur essaie d'optimiser la taille de manière agressive.
-
La
libstd
précompilée est optimisée pour la vitesse, pas pour la taille. -
Il n'est pas possible de retirer des portions de
libstd
qui ne sont pas utilisé dans une application en particulier (par exemple LTO et le comportement de panic).
C'est ici que build-std
se retrouve utile. La feature build-std
est capable de compiler libstd
dans votre application depuis la source. Il le fait avec le composant rust-src
que rustup
fournit commodément.
Installez la suite d'outils appropriée et le composant rust-src
:
$ rustup toolchain install nightly
$ rustup component add rust-src --toolchain nightly
Compiler en utilisant build-std
:
# Afficher le triplet de votre machine
$ rustc -vV
...
host: x86_64-apple-darwin
# Utilisez ce triplet quand vous compilez avec build-std.
# Ajoutez l'option =std,panic_abort pour permettre à l'option panic = "abort" de Cargo.toml de fonctionner.
# Voir: https://github.com/rust-lang/wg-cargo-std-aware/issues/56
$ RUSTFLAGS="-Zlocation-detail=none" cargo +nightly build -Z build-std=std,panic_abort --target x86_64-apple-darwin --release
Sur macOS, la taille finale du binaire est réduite à 51 Ko.
Supprimer le formatting des chaînes de caractères de panic
avec panic_immediate_abort
- Version Rust Minimale : Nightly
Même si panic = "abort"
est spécifié dans Cargo.toml
, rustc
il inclut toujours par défaut les chaînes de caractère et le code de formatage du panic dans le binaire final. Une feature instable panic_immediate_abort
a été mergée dans le compileur nightly
rustc
pour addresser cela.
Pour l'utiliser, répétez les instructions au dessus pour utiliser build-std
, mais passez également l'option suivante -Z build-std-features=panic_immediate_abort
.
$ cargo +nightly build -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort \
--target x86_64-apple-darwin --release
Sur macOS, la taille finale du binaire est réduite à 30 Ko.
Supprimer core::fmt
avec #![no_main]
et un usage prudent de libstd
- Version Rust Minimale : Nightly
Un exemple de projet est disponible de le dossier
no_main
.
Cette section est en partie possible grâce à @vi
Jusqu'à présent, nous n'avons pas restreint les utilitaires de libstd
que nous utilisions. Dans cette section, nous allons restreindre notre utilisation de libstd
afin de réduire encore plus la taille des binaires.
Si vous voulez un exécutable de moins de 20 kilo-octets, le code de formatage des chaînes de Rust, core::fmt
doit être supprimé. panic_immediate_abort
ne supprime que certaines utilisations de ce code. Il y a beaucoup d'autres codes qui utilisent le formatage dans certains cas. Cela inclut le code "pre-main" de Rust dans libstd
.
En utilisant un point d'entrée C (en ajoutant l'attribut # ![no_main]
), en gérant stdio manuellement, et en analysant soigneusement les morceaux de code que vous ou vos dépendances incluez, vous pouvez parfois utiliser libstd
tout en évitant core::fmt
.
Il faut s'attendre à ce que le code soit plus complexe et non portable, avec plus de unsafe{}
que d'habitude. Cela ressemble à no_std
, mais avec libstd
.
Commencez avec un exécutable vide, assurez-vous que xargo bloat --release --target=...
ne contient pas core::fmt
ou quelque chose relatif au padding. Ajoutez (ou décommentez) un peu de code et confirmez que xargo bloat
relève beaucoup plus d'information désormais. Examinez le code source que vous venez d'ajouter. Il est probable qu'une librairie externe ou une nouvelle fonction libstd
est utilisée. Tenez-en compte dans votre processus d'évaluation (cela vous force à utiliser la section [replace]
et peut-être chercher dans libstd
) et essayez de comprendre pourquoi cela prend plus de place que ça ne devrait. Choisisez une alternative ou patchez les dépendances pour éviter des features non nécessaires. Décommentez un peu plus le code, et continuez à débugger avec xargo bloat
.
Sur macOS, la taille finale du binaire est réduite à 8 Ko.
Supprimer libstd
avec #![no_std]
- Version Rust Minimale : 1.30
Un exemple de projet est disponible de le dossier
no_std
.
Jusqu'à présent, notre application utilisait la bibliothèque standard Rust, libstd
. libstd
fournit de nombreuses APIs cross-plateforme pratique et bien testées. Mais si un utilisateur veut réduire la taille d'un programme binaire à une taille équivalente à celle d'un programme C, il est possible de ne dépendre que de libc
.
Il est important de comprendre que cette approche présente de nombreux inconvénients. Par exemple, vous devrez probablement écrire beaucoup de code unsafe
et perdre l'accès à la majorité des crates Rust qui dépendent de libstd
. Néanmoins, il s'agit d'une option (bien qu'extrême) pour réduire la taille des binaires.
Un binaire réduit de cette manière est autour de 8 Ko.
#![no_std]
#![no_main]
extern crate libc;
#[no_mangle]
pub extern "C" fn main(_argc: isize, _argv: *const *const u8) -> isize {
// Puisque nous transmettons une chaîne de caractères C, le caractère nul final est obligatoire.
const HELLO: &'static str = "Hello, world!\n\0";
unsafe {
libc::printf(HELLO.as_ptr() as *const _);
}
0
}
#[panic_handler]
fn my_panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
Compresser le binaire
Jusqu'à présent, toutes les techniques de réduction de la taille étaient spécifiques à Rust. Cette section décrit un outil de compression agnostique au language qui permet de réduire davantage la taille des binaires.
UPX est un outil puissant permettant de créer un fichier binaire autonome et compressé sans exigence supplémentaire en matière d'exécution. Il prétend réduire la taille des binaires de 50 à 70 %, mais le résultat réel dépend de votre exécutable.
$ upx --best --lzma target/release/min-sized-rust
Il convient de noter qu'il est arrivé que des binaires contenant des fichiers UPX soient détectés par des logiciels antivirus basés sur une méthode heuristique, car les logiciels malveillants utilisent souvent UPX.
Outils
cargo-bloat
- Déterminez ce qui occupe le plus d'espace dans votre exécutable.cargo-unused-features
- Trouvez et éliminez de votre projet les features activées mais potentiellement inutilisées.momo
-proc_macro
librairie utilisée pour limiter l'empreinte des méthodes génériques dans le code.- Twiggy - Un profileur de taille de code pour Wasm.
Conteneurs
Il est parfois avantageux de déployer Rust dans un container (par exemple Docker). Il existe plusieurs ressources intéressantes pour aider à créer des images de conteneurs de taille minimale qui exécutent les binaires Rust.
- L'image
rust:alpine
officielle - mini-docker-rust
- muslrust
- docker-slim - Minifier les images Docker
Références
- 151-byte static Linux binary in Rust - 2015
- Why is a Rust executable large? - 2016
- Tiny Rocket - 2018
- Formatting is Unreasonably Expensive for Embedded Rust - 2019
- Tiny Windows executable in Rust - 2019
- Making a really tiny WebAssembly graphics demos - 2019
- Reducing the size of the Rust GStreamer plugin - 2020
- Optimizing Rust Binary Size - 2020
- Minimizing Mender-Rust - 2020
- Optimize Rust binaries size with cargo and Semver - 2021
- Tighten rust’s belt: shrinking embedded Rust binaries - 2022
- Avoiding allocations in Rust to shrink Wasm modules - 2022
- A very small Rust binary indeed - 2022
min-sized-rust-windows
- Astuces spécifiques à Windows pour réduire la taille des binaires- Shrinking
.wasm
Code Size