默认情况下让 Rust 二进制文件更小
Rust 的 “hello world” 二进制文件长期以来因体积达到数 MB 而受到批评,许多人认为这削弱了其“零成本抽象”的口碑,并让习惯于小型 C 可执行文件的开发者望而却步。Cargo 的一项新改动会在未请求 debuginfo 时默认剥离标准库调试符号,将典型 release 二进制大小大约削减 90%(降到几百 KB),同时仍为开发构建保留完整符号。评论者讨论了在现代系统与嵌入式/受限环境中二进制大小到底有多重要,并权衡这项改动与回溯、panic 行为、链接时优化以及 Rust 静态链接、无稳定 ABI 设计之间的取舍。
第一印象与二进制大小作为信号
- 一些评论者表示,一个多 MB 的“hello world”会立刻让评估者失去兴趣,尤其是习惯于小型二进制文件的 C/C++ 开发者。
- 另一些人认为,初始大小主要是固定成本,并不能预测程序变大后的边际增长。
- 对于“hello world 大小”是否是抽象开销的好代理,存在争论:有人总体上认为是,另一些人则指出 Rust 通过去除调试信息从 4MB 降到 400KB,说明大小可能主要由工具链选择决定,而不是语言开销。
一个约 415KB 的 Rust “hello world” 里有什么?
- 提到的主要贡献因素:
- 回溯支持:栈回溯、DWARF 解析、名称反混淆、路径处理、压缩的 ELF 片段。
- I/O 机制:带缓冲的 stdout、同步、分配器、向量实现。
- 格式化,尤其是浮点数,依赖大量数据表和边界情况处理。
- std 的静态链接(为了可移植性以及缺乏稳定的 Rust ABI)本质上会比动态链接的 C 程序拉入更多内容。
去除调试信息与堆栈跟踪
- 之前的行为:即使是 release 二进制,也会嵌入 std 的调试信息,把“hello world”膨胀到约 4MB。
- 新默认值:在未请求 debuginfo 时使用
strip = "debuginfo",移除 std 的 DWARF,但保留运行时行为。 - 后果:release 回溯会丢失行号,但评论者指出,如果没有用户代码的调试信息,这些行号通常也不太有用。
- 有人担心符号丢失;也有人强调外部/拆分调试信息以及 debuginfod 才是长期正确的模式。
手动大小调优与权衡
- 常见方案:
opt-level="z"、lto=true、codegen-units=1、panic="abort",再加上用 panic_abort / panic_immediate_abort 构建 std 并进行 strip。这样可以达到几十 KB。 - 讨论的权衡:
- 体积优化构建会导致编译更慢,有时运行也更慢。
panic="abort"会移除展开、panic 时析构,以及某些恢复模式;有人把它类比为-fno-exceptions。- 把所有内容都 strip 掉(不仅仅是 debuginfo)可能会破坏二进制;
--strip-unneeded被特别提到。
何时二进制大小重要(何时不重要)
- 有人认为,对大多数桌面/服务器用途来说,任何小于 1MB 的体积都够了;性能和编译时间更重要。
- 另一些人,尤其是在嵌入式、资源受限设备的 Linux 环境以及低资源容器中,强调 KB 级别会累积,而且可能是硬性约束。
- 还有环境/效率角度:大型二进制和浪费的工作被视为系统性的臃肿。
静态链接 vs 动态链接与 ABI 稳定性
- Rust 默认静态链接 std,因为其 ABI 不稳定;动态 Rust 库是可能的,但不被鼓励。
- 可以使用 C ABI 来构建可被其他语言调用的稳定共享对象。
- 嵌入式 Rust 经常使用
#![no_std],这会绕开其中大部分开销;但许多“嵌入式” Linux 系统仍然关心完整 std 二进制的大小。
项目流程与态度
- 有人批评这种“显而易见”的问题拖了大约 7 年,认为这说明关注点偏向特性而非基础。
- 也有人认为这很正常:受影响的用户并不多,已有变通方法,而且还有许多更紧急的问题需要优先处理。
- 大体共识是:即使问题已有已知变通办法,改进默认行为对 Rust 的完善度和外界认知也很重要。