为什么Rust可执行文件如此庞大?

346

我觉得文档的前两章对语言的定义和方法非常有趣。因此,我决定动手尝试并以“Hello, world!”开始。

顺便说一句,我是在 Windows 7 x64 上进行的。

fn main() {
    println!("Hello, world!");
}

执行cargo build命令并查看结果位于targets\debug目录下,我发现生成的.exe文件大小为3MB。经过一番搜索(很难找到cargo命令行标志的文档...),我找到了--release选项并创建了发布版本的构建。让我惊讶的是,.exe文件大小只减小了一个微不足道的量:2.99MB而不是3MB。

我本来期望系统编程语言会产生紧凑的代码。

有人可以详细说明一下Rust在编译成什么,它如何可能从一个三行程序产生如此巨大的镜像?它是否正在编译成虚拟机?我是否错过了strip命令(发布版内部的调试信息)?是否还有其他任何可以帮助理解正在发生的事情的东西?


12
我认为3Mb不仅包含了“Hello World”,还包含了平台所需的全部环境。这在Qt中也是一样的。这并不意味着如果你编写了一个6行程序,它的大小就会变成6 Mb。它将保持在3Mb,并且之后增长速度将非常缓慢。 - Andrei Nikolaenko
30
我知道这一点。但是这暗示着他们处理库的方式可能不同于 C,只添加图像所需的内容,或者其他情况正在发生。 - BitTickler
2
我发现对于静态库,-C opt-level=3 -C lto有助于显著减小其大小。 - Learn OpenGL ES
28
现在这是否已经过时了?使用 rustc 版本 1.35.0,并没有使用命令行选项,我得到的可执行文件大小为 137kb。现在它是否自动编译为动态链接,或者在此期间发生了其他事情? - itmuckel
显示剩余8条评论
7个回答

308

默认情况下,Rust编译器为执行速度、编译速度和调试方便(例如包含符号)进行优化,而不是追求最小的二进制大小。

有关减小Rust二进制文件大小的所有方法概述,请参阅我的min-sized-rust GitHub存储库。

目前减小二进制大小的高级步骤如下:

  1. 使用 Rust 1.32.0 或更新版本(默认情况下不包括 jemalloc
  2. 将以下内容添加到 Cargo.toml 文件中:
[profile.release]
opt-level = 'z'     # Optimize for size
lto = true          # Enable link-time optimization
codegen-units = 1   # Reduce number of codegen units to increase optimizations
panic = 'abort'     # Abort on panic
strip = true        # Strip symbols from binary*

* strip = true 需要 Rust 1.59+。在旧版本的 Rust 上,需要手动运行 strip 命令来剥离二进制文件。

  1. 使用 cargo build --release 命令构建发布模式下的程序。

Rust 的 nightly 版本可以做更多的事情,但由于使用了不稳定的特性,相关信息请参见 min-sized-rust

你也可以使用 #![no_std] 来移除 Rust 的标准库。详见min-sized-rust


21
哇,这将我的可执行文件从50MB缩小到了6MB!没想到会有如此大的改善。 - ThatCoolCoder
1
使用 RUSTFLAGS='-C strip=symbols' cargo build --release 进行编译,以稳定的 rustc 标志剥离二进制文件。 - Elias
13
请注意,这些设置会对性能产生影响,因此请确保这不会对您的用例造成问题。 - outside2344

265

Rust使用静态链接来编译其程序,这意味着即使是最简单的Hello world!程序所需的所有库都将被编译进可执行文件中,其中包括Rust运行时。

要强制Rust动态链接程序,请使用命令行参数-C prefer-dynamic; 这将导致文件大小大大减小但是也要求Rust库(包括其运行时)在运行时可用于您的程序。这实际上意味着如果计算机没有它们,您将需要提供它们,占用的空间比您原始的静态链接程序更多。

为了可移植性,如果您要向别人分发程序,则建议您以与您一直以来一样的方式静态链接Rust库和运行时。


60
我认为静态链接并不能解释这个巨大的HELLO-WORLD。它不应该只连接实际使用的库的部分,而HELLO-WORLD实际上几乎没有使用任何东西。 - MWB
15
BitTickler cargo rustc [--debug 或 --release] -- -C prefer-dynamic - Zoey Mertes
4
谢谢你。我一直在追踪这个相关的RFC。这真的很遗憾,因为Rust也面向系统编程。 - Franklin Yu
7
@Nulik:是的,这是默认情况,但这是因为Rust默认使用静态构建(包括所有依赖项,包括运行时),而Go链接其运行时动态库。在我的CentOS 7系统上,Go的helloworld编译出的大小约为76K,但除了标准内容外,它还会动态依赖于libgo.so运行时库,该库大小超过47M。默认情况下,Rust的helloworld(使用cargo new创建)没有任何独特的动态依赖项,在一个1.6M的可执行文件中包含除基本C运行时内容以外的一切;通过调整(优化大小、使用LTO、在出现故障时中止),其大小将缩小到0.6M。 - ShadowRanger
8
-C prefer-dynamic 选项可将发布版本(仅启用大小优化;无法使用 LTO 或在 panic 时中止)的体积缩小至 8.8K,但需要一个新的 4.7M 动态依赖。因此,若进行公平比较,Rust 的体积更小;动态链接时只有原来的十分之一,同时还依赖一个体积也是原来十分之一的运行时。 - ShadowRanger
显示剩余13条评论

110

我没有任何Windows系统可以尝试,但在Linux上,静态编译的Rust hello world实际上比等价的C小。如果您看到巨大的大小差异,可能是因为您将Rust可执行文件静态链接而将C文件动态链接。

使用动态链接,您需要考虑所有动态库的大小,而不仅仅是可执行文件的大小。

因此,如果您想进行苹果与苹果的比较,需要确保两者都是动态的或都是静态的。不同的编译器将具有不同的默认值,因此您不能仅依赖于编译器默认值来产生相同的结果。

如果您感兴趣,这里是我的结果:

-rw-r--r-- 1 aij aij     63 Apr  5 14:26 printf.c
-rwxr-xr-x 1 aij aij   6696 Apr  5 14:27 printf.dyn
-rwxr-xr-x 1 aij aij 829344 Apr  5 14:27 printf.static
-rw-r--r-- 1 aij aij     59 Apr  5 14:26 puts.c
-rwxr-xr-x 1 aij aij   6696 Apr  5 14:27 puts.dyn
-rwxr-xr-x 1 aij aij 829344 Apr  5 14:27 puts.static
-rwxr-xr-x 1 aij aij   8712 Apr  5 14:28 rust.dyn
-rw-r--r-- 1 aij aij     46 Apr  5 14:09 rust.rs
-rwxr-xr-x 1 aij aij 661496 Apr  5 14:28 rust.static

使用gcc(Debian 4.9.2-10)4.9.2和rustc 1.0.0-nightly(d17d6e7f1 2015-04-02)(构建于2015-04-03)编译了这些文件,都是使用默认选项,并在gcc中使用-static和在rustc中使用-C prefer-dynamic

我有两个版本的C hello world,因为我认为使用puts()可能会链接更少的编译单元。

如果您想要在Windows上尝试复制它,请使用以下源代码:

printf.c:

#include <stdio.h>
int main() {
  printf("Hello, world!\n");
}

puts.c:

#include <stdio.h>
int main() {
  puts("Hello, world!");
}

Rust.rs

fn main() {
    println!("Hello, world!");
}

请记住,不同数量的调试信息或不同的优化级别也会产生影响。但是,如果您看到巨大的差异,则可能是由于静态链接与动态链接之间的区别。


59
GCC 足够聪明,能够自动将 printf 替换为 puts,因此结果是相同的。 - bluss
18
截至2018年,如果您想要进行公正比较,请记得"剥离"可执行文件。例如,在我的系统上,一个简单的Rust程序需要占用5.3MB的空间,但是当您移除所有调试符号等内容时,其大小将降低到不到原来的10%。 - Matti Virkkunen
2
@MattiVirkkunen:2020年仍然如此;自然大小似乎更小(远非5.3M),但符号与代码的比率仍然相当极端。在CentOS 7上使用Rust 1.34.0的默认选项进行调试构建,使用strip -s剥离后,从1.6M降至190K。发布构建(默认加上opt-level='s'lto=truepanic='abort'以最小化大小)从623K降至158K。 - ShadowRanger

58

当使用Cargo进行编译时,您可以使用动态链接:

cargo rustc --release -- -C prefer-dynamic

现在,由于它是动态链接的,因此这将大大减小二进制文件的大小。

至少在Linux上,你还可以使用strip命令剥离二进制文件的符号:

strip target/release/<binary>

这将大致减少大多数二进制文件的大小一半。


22
一些数据统计,标准发布版本的hello world(Linux x86_64)大小为3.5兆字节。如果使用prefer-dynamic参数,则大小为8904字节,经过精简处理后为6392字节。 - Zitrax
7
应该添加一条注释,说明它不适合分发。 - TheTechRobo the Nerd
1
在Windows上运行此代码会导致错误。 "代码执行无法继续,因为找不到std-fd55ee3d3a94e250.dll。重新安装程序可能会解决此问题。" - Jeferson Tenorio
1
使用 opt-level='z'、lto='true'、codegen-units='1'、panic='abort' 参数编译后,再通过 strip -s <name_of_executable> 去除符号表等信息,可将文件大小缩小至约 200KB。这对于大多数对文件大小敏感的人来说已经足够了。 - zombiesauce

3
#![no_main]
#![no_std]

#[link(name = "msvcrt", kind = "dylib")]
extern {
    fn puts(ptr: *const u8); // i8 or u8 doesn't matter in this case
}

#[no_mangle]
unsafe extern fn main() {
    puts("Hello, World!\0".as_ptr());
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

使用下一个配置文件
[profile.release]
debug = false
strip = true
opt-level = 'z'
codegen-units = 1
lto = true
panic = 'abort'

使用-r命令,可以得到9 kb的输出,这是C语言相关的。
#include <stdio.h>

main() {
    puts("Hello, World!");
}

使用GCC和-Os编译时,生成的文件大小为48 kb,而使用TCC编译时只有2 kb。相当令人印象深刻,不是吗?


为什么这里的main函数是“不安全”的? - BitTickler
1
@bittickler,直接翻译:)实际上,由于puts是外部函数,我们需要使用unsafe来调用它。在这种情况下,将函数设为不安全比输入块更容易。 - Miiao

1

安装 Rust Nightly - rustup toolchain install nightly, rustup default nightly

现在,在您的项目中的所有 Cargo.toml 文件中进行以下更改。

在 Cargo.toml 顶部的 [package] 之前添加 cargo-features = ["strip"]

在底部或者在 [dependencies][package] 之间添加,

[profile.release]
# strip = true  # Automatically strip symbols from the binary.
opt-level = "z"  # Optimize for size.
lto = true  # Enable link time optimization
codegen-units = 1  # Reduce parallel code generation units

现在使用RUSTFLAGS='-C link-arg=-s' cargo build --release进行构建。

我发现这些链接很有用 - https://collabora.com/news-and-blog/blog/2020/04/28/reducing-size-rust-gstreamer-plugin/https://github.com/johnthagen/min-sized-rusthttps://arusahni.net/blog/2020/03/optimizing-rust-binary-size.html


1
现在可以使用RUSTFLAGS='-C strip=symbols' cargo build --release在稳定的 Rust 中进行剥离操作。 - Elias

-19

这是一个特性,而不是一个错误!

您可以在程序中指定库版本(在项目关联的Cargo.toml文件中),以确保库版本兼容性(甚至是隐式的)。然而,这要求将特定的库静态链接到可执行文件,生成大型运行时映像。

嘿,现在已经不是1978年了 - 许多人的计算机内存超过2 MB了 :-)


22
“指定库的版本[...]要求特定库进行静态链接” — 不,这并不是必须的。存在许多代码使用动态链接来连接确切版本的库。 - Shepmaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接