此文演示了如何减小 Rust 生成的二进制文件大小。

默认情况下,Rust 对执行速度、编译速度和易于调试而不是二进制大小进行优化,因为对于绝大多数应用程序来说,这是理想的。对于想要优化二进制大小的开发人员来说,Rust 提供了实现此目标的机制。

在发布模式下构建

Minimum Rust: 1.0

默认情况下, cargo build 在调试模式下构建 Rust 二进制文件。调试模式会禁用许多优化,这有助于调试器(以及运行它们的 IDE)提供更好的调试体验。调试时构建的二进制文件可比发布时构建的二进制文件大 30% 或更多。

若要最小化二进制大小,请在发布模式下构建:

$ cargo build --release

strip 二进制符号

OS: *nix Minimum Rust: 1.59

默认情况下,在 Linux 和 macOS 上,符号信息包含在编译 .elf 文件中。正确执行二进制文件不需要此信息。

Cargo 可以配置为自动 strip 二进制文件。按这种方式修改 Cargo.toml

[profile.release]
strip = true  # 自动从二进制文件去除符号信息.

在 Rust 1.59 之前,直接在 .elf 文件上运行 strip

$ strip target/release/min-sized-rust

针对尺寸进行优化

Minimum Rust: 1.28

Cargo 发布模式默认优化级别为 3,这会优化二进制文件的执行速度。要指示 Cargo 针对二进制大小进行优化,请在 Cargo.toml 使用 z 优化级别:

[profile.release]
opt-level = "z"  #  二进制文件大小优化

启用链接时优化 (LTO)

Minimum Rust: 1.0

默认情况下,Cargo 指示编译单元进行单独编译和优化。LTO指示链接器在 link 阶段进行优化。例如,这可以删除无用代码,并且通常可以减小二进制大小。

Cargo.toml 中启用 LTO:

[profile.release]
lto = true

移除 Jemalloc

Minimum Rust: 1.28 Maximum Rust: 1.31

从 Rust 1.32 开始, jemalloc 默认情况下被删除。如果使用 Rust 1.32 或更高版本,则无需执行任何操作来减小有关此功能的二进制大小。

在 Rust 1.32 之前,为了提高某些平台的性能,Rust 捆绑了 jemalloc,这是一个通常优于默认系统内存分配器的分配器。然而,捆绑 jemalloc 会在生成的二进制文件中增加大约 200KB。

要在 Rust 1.28 - Rust 1.31 上删除 jemalloc ,请将此代码添加到 的 main.rs 顶部:

use std::alloc::System;
  
#[global_allocator]
static A: System = System;

减少并行代码生成单元以提高优化

默认情况下,Cargo 为发布版本指定 16 个并行代码生成单元。这缩短了编译时间,但会阻止某些优化。

Cargo.toml 中将其设置为 1 以允许最大尺寸缩减优化:

[profile.release]
codegen-units = 1

Abort on Panic

Minimum Rust: 1.10

注意:到目前为止,讨论的减小二进制大小的功能对程序的行为没有影响(仅影响其执行速度)。 然而此功能会对程序的行为产生影响。

默认情况下,当 Rust 代码遇到必须调用 panic!() 的情况时,它会展开堆栈并生成有用的回溯。但是,展开代码确实需要额外的二进制大小。 rustc 可以指示立即中止而不是展开,这样就不需要这个额外的展开代码。

Cargo.toml 中启用此功能:

[profile.release]
panic = "abort"

build-std 方式优化 libstd

Minimum Rust: Nightly

注意:另请参见 build-std 的前身 Xargo。Xargo 目前处于维护状态。

示例项目位于 build_std 文件夹中。

Rust 提供了标准库 ( libstd ) 的预构建副本及其工具链。这意味着开发人员不需要在每次构建应用程序时都进行构建 libstd 。 而是将 libstd 静态链接到二进制文件。

虽然这非常方便,但如果开发人员试图积极优化大小,则有几个缺点。

  1. 预构建 libstd 针对速度进行了优化,而不是大小。

  2. 不可能删除特定应用程序中未使用的部分 libstd (例如 LTO 和 panic)。

这就是 build-std 的用武之地 。build-std 能够使用应用程序从源代码进行编译 libstd 。它使用 rustup 提供的 rust-src 组件来执行此操作。

安装相应的工具链和 rust-src 组件:

$ rustup toolchain install nightly
$ rustup component add rust-src --toolchain nightly

使用 build-std 构建:

# 查看目标宿主的架构. 
$ rustc -vV
...
host: x86_64-apple-darwin

# Use that target triple when building with build-std.
# Add the =std,panic_abort to the option to make panic = "abort" Cargo.toml option work.
# See: https://github.com/rust-lang/wg-cargo-std-aware/issues/56
$ cargo +nightly build -Z build-std=std,panic_abort --target x86_64-apple-darwin --release

在 macOS 上,最终的二进制大小能减少到 51KB。

删除 panic 字符串格式化方法 panic_immediate_abort

Minimum Rust: Nightly

即使在 Cargo.toml 中指定 panic = "abort"rustc 默认情况下仍将在最终二进制文件中包含 panic 字符串和格式化代码。nightly rustc 编译器中已合并一个不稳定的 panic_immediate_abort 功能来解决此问题。

要使用它,请重复上述说明以使用 build-std ,同时也要传递以下 -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

在 macOS 上,最终二进制大小减少到 30KB。

删除 core::fmt #![no_main] 并小心使用 libstd

Minimum Rust: Nightly

示例项目位于 no_main 文件夹中。

本节部分由@vi

到目前为止,我们还没有限制我们使用的实用程序 libstd 。在本节中,我们将限制我们的使用 libstd,以进一步减小二进制大小。

如果你想要一个小于 20 KB 的可执行文件,必须删除 Rust 的字符串格式化代码 core::fmtpanic_immediate_abort 仅删除此代码的某些用法。在某些情况下,还有很多其他代码使用格式。这包括 Rust 在 libstd 中的 pre-main`代码。

通过使用 C 入口点(通过添加 #![no_main] 属性)、手动管理 stdio 并仔细分析您或您的依赖项包含哪些代码块,您有时可以同时利用 libstd 来避免臃肿的 core::fmt

虽然代码会扁的很笨拙且不可移植,同时更多的使用 unsafe{}。感觉像 no_std ,但有了 libstd .

从空的可执行文件开始,确保 xargo bloat --release --target=... 不包含 core::fmt 或有关填充的内容。看到现在 xargo bloat 报告了更多。查看刚刚添加的源代码。可能使用了某些外部板条箱或新功能 libstd 。在您的审查过程中递归(它需要 [replace] Cargo 依赖项,也许需要深入研究 libstd ),找出为什么它的重量超过应有的重量。选择替代方法或修补依赖项以避免不必要的功能。取消注释更多代码,调试分解大小 xargo bloat 等等。

在 macOS 上,最终的二进制文件减少到 8KB。

删除 libstd #![no_std]

Minimum Rust: 1.30

示例项目位于 no_std 文件夹中。

到目前为止,我们的应用程序使用的是 Rust 标准库 libstdlibstd 提供了许多方便、经过良好测试的跨平台 API 和数据类型。但是,如果用户希望将二进制大小减小到等效的 C 程序大小,则可以仅依赖 libc

重要的是要了解这种方法有很多缺点。首先,您可能需要编写大量 unsafe 代码,并且无法访问大多数依赖于 libstd 的库. 尽管如此,这是减少二进制大小的一种(尽管极端)选择。

以这种方式构建的 strip 二进制文件约为 8KB。

#![no_std]
#![no_main]

extern crate libc;

#[no_mangle]
pub extern "C" fn main(_argc: isize, _argv: *const *const u8) -> isize {
    // 因为我们需要一个 cstring 传到 printf 函数中, 因此字符串最后必须加上 null 字符.
    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 {}
}

压缩二进制文件

到目前为止,所有尺寸减小技术都是特定于 Rust 的。本节介绍与语言无关的二进制打包工具,该工具是进一步减小二进制大小的选项。

UPX 是一个强大的工具,用于创建独立的压缩二进制文件,无需额外的运行时要求。它声称通常会将二进制大小减少 50-70%,但实际结果取决于您的可执行文件。

$ upx --best --lzma target/release/min-sized-rust

应该注意的是,有时 UPX 打包的二进制文件会标记基于启发式的防病毒软件,因为恶意软件经常使用 UPX。

工具

  • cargo-bloat - 找出什么占用了可执行文件中的大部分空间。
  • cargo-unused-features - 从项目中查找并修剪已启用但可能未使用的功能标志。
  • momo - proc_macro crate 以帮助检查泛型方法的代码足迹。
  • Twiggy - Wasm的代码大小分析器。

容器

有时将 Rust 部署到容器(例如 Docker)中是有利的。有几个很好的现有资源可以帮助创建运行 Rust 二进制文件的最小大小的容器映像。

引用