最小化 Rust 二进制文件大小
此文演示了如何减小 Rust 生成的二进制文件大小。
默认情况下,Rust 对执行速度、编译速度和易于调试而不是二进制大小进行优化,因为对于绝大多数应用程序来说,这是理想的。对于想要优化二进制大小的开发人员来说,Rust 提供了实现此目标的机制。
在发布模式下构建
默认情况下, cargo build 在调试模式下构建 Rust 二进制文件。调试模式会禁用许多优化,这有助于调试器(以及运行它们的 IDE)提供更好的调试体验。调试时构建的二进制文件可比发布时构建的二进制文件大 30% 或更多。
若要最小化二进制大小,请在发布模式下构建:
$ cargo build --release
strip 二进制符号
默认情况下,在 Linux 和 macOS 上,符号信息包含在编译 .elf 文件中。正确执行二进制文件不需要此信息。
Cargo 可以配置为自动 strip 二进制文件。按这种方式修改 Cargo.toml :
[profile.release]
strip = true # 自动从二进制文件去除符号信息.
在 Rust 1.59 之前,直接在 .elf 文件上运行 strip :
$ strip target/release/min-sized-rust
针对尺寸进行优化
Cargo 发布模式默认优化级别为 3,这会优化二进制文件的执行速度。要指示 Cargo 针对二进制大小进行优化,请在 Cargo.toml 使用 z 优化级别:
[profile.release]
opt-level = "z" # 二进制文件大小优化
启用链接时优化 (LTO)
默认情况下,Cargo 指示编译单元进行单独编译和优化。LTO指示链接器在 link 阶段进行优化。例如,这可以删除无用代码,并且通常可以减小二进制大小。
在 Cargo.toml 中启用 LTO:
[profile.release]
lto = true
移除 Jemalloc
从 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
注意:到目前为止,讨论的减小二进制大小的功能对程序的行为没有影响(仅影响其执行速度)。 然而此功能会对程序的行为产生影响。
默认情况下,当 Rust 代码遇到必须调用 panic!() 的情况时,它会展开堆栈并生成有用的回溯。但是,展开代码确实需要额外的二进制大小。 rustc 可以指示立即中止而不是展开,这样就不需要这个额外的展开代码。
在 Cargo.toml 中启用此功能:
[profile.release]
panic = "abort"
build-std 方式优化 libstd
注意:另请参见
build-std的前身 Xargo。Xargo 目前处于维护状态。
示例项目位于
build_std文件夹中。
Rust 提供了标准库 ( libstd ) 的预构建副本及其工具链。这意味着开发人员不需要在每次构建应用程序时都进行构建 libstd 。 而是将 libstd 静态链接到二进制文件。
虽然这非常方便,但如果开发人员试图积极优化大小,则有几个缺点。
预构建
libstd针对速度进行了优化,而不是大小。不可能删除特定应用程序中未使用的部分
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
即使在 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
示例项目位于
no_main文件夹中。
本节部分由@vi
到目前为止,我们还没有限制我们使用的实用程序 libstd 。在本节中,我们将限制我们的使用 libstd,以进一步减小二进制大小。
如果你想要一个小于 20 KB 的可执行文件,必须删除 Rust 的字符串格式化代码 core::fmt 。panic_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]
示例项目位于
no_std文件夹中。
到目前为止,我们的应用程序使用的是 Rust 标准库 libstd 。libstd 提供了许多方便、经过良好测试的跨平台 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_macrocrate 以帮助检查泛型方法的代码足迹。Twiggy- Wasm的代码大小分析器。
容器
有时将 Rust 部署到容器(例如 Docker)中是有利的。有几个很好的现有资源可以帮助创建运行 Rust 二进制文件的最小大小的容器映像。
引用
- Rust 生成 151 字节的静态 Linux 二进制文件 - 2015
- 为什么 Rust 可执行文件很大?- 2016
- 独立的Rust二进制文件 - 2018
- 小火箭 - 2018
- 格式化对于嵌入式 Rust 来说过于昂贵 - 2019
- Rust生成的小的 Windows 可执行文件 - 2019
- 制作一个非常小的 WebAssembly 图形演示程序 - 2019
- 减小 Rust GStreamer 插件的大小 - 2020
- 优化 Rust 二进制大小 - 2020
- 勒紧 Rust 的裤腰带:缩小嵌入式 Rust 二进制文件 - 2022
- 避免在 Rust 中分配以缩小 Wasm 模块 - 2022
- 一个非常小的 Rust 二进制文件 - 2022
- min-sized-rust-windows - 特定于Windows的技巧,以减少二进制大小