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

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

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

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)

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

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

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

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

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.

  1. La libstd précompilée est optimisée pour la vitesse, pas pour la taille.

  2. 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

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

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]

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

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.

Références