最小化 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_macro
crate 以帮助检查泛型方法的代码足迹。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的技巧,以减少二进制大小