自举 Rust 被认为有害

Lobsters Hottest 新闻

摘要

对 Rust 编译器自举过程的批判性分析,指出与 OCaml 等其他语言相比,其体积过大和依赖臃肿的问题,并主张采用更轻量的方法。

<p><a href="https://lobste.rs/s/pjpqxb/bootstrapping_rust_considered_harmful">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/10 17:49

# Bootstrapping Rust 被认为有害 - NTECS Consulting 来源:https://www.ntecs.de/blog/2026-02-01-bootstrapping-rust-considered-harmful/ 2026年2月1日 你是否曾尝试从源码编译 Rust?*为什么*呢?你可能会问: - 可能是因为你的操作系统没有通过 `rustup` 提供 Rust - 可能是因为你不信任二进制下载 - 可能纯粹出于好奇 这应该不难,对吧?好吧……本文试图回答这个问题,以及为什么我认为 *Rust 需要大幅改变其引导过程*。在深入细节之前,让我们看看我在 DragonFly BSD (https://https//www.dragonflybsd.org/) 上构建 Rust 1.81 花费了多长时间,与其他语言相比(感谢 R 和 ggplot2 提供的图表): **警告**:这是一篇关于 Rust 的“吐槽”!不是针对语言本身 —— Rust 很美 —— 而是针对其当前的实现。更准确地说,是关于 Rust 官方实现(不是 gcc 版本)的*引导过程*以及从源码构建 Rust 所需的*大量依赖*的吐槽。如果你写的是“极简”软件 —— 任何在终端上运行的程序 —— *请*注意这种“臃肿”,并考虑使用依赖更少、资源消耗更低的语言 —— 或者等待更轻量的“gcc 版 Rust”。谢谢! OCaml 在本文中作为参考点,并不是因为它更好。OCaml 同样复杂,但其引导过程实现得很好:使用 `./configure && make && make install` 只需三分钟即可从源码构建完整的 OCaml 工具链。 ## 从源码安装 Rust 引用 Rust 的 README.md (https://github.com/rust-lang/rust/blob/main/README.md) 关于“从源码安装”的内容: > 如果你真的想从源码安装(尽管不推荐),请参阅 INSTALL.md。 尽管有此警告,我还是下载了 Rust 1.84.1 的源码 `rustc-1.84.1-src.tar.gz`(注意在上面图表中我展示的是 Rust 1.81.1 的构建时间,因为我无法构建 Rust 1.84.1)。压缩后的 tarball 大小为 699 MB,微不足道。嘿,这还能装进一张 CD-ROM 吗?不,不能! 作为对比,OCaml 的源码(版本 5.4.0)压缩后只有 6.0 MB,差了两个数量级,你一定会惊呼!两个数量级…… 公平地说,这 699 MB 的源码包含了构建 Rust 所需的*所有*源码,包括大约一千万行 LLVM 代码和三个不同版本的 OpenSSL。 嗯,确实如此。所有源码都包含在内,但*不包含*二进制 Rust 编译器(以及 `cargo` 二进制文件),而这两者都是构建 Rust 从源码所必需的!!!*等等,什么?*这不是一个*先有鸡还是先有蛋*的问题吗?我们实际上正试图从源码构建 Rust……!?是的,确实存在。我们稍后再谈这个问题…… 让我们继续,解压这个大而美的 tarball。2 分 14 秒后,`tar xvzf rustc-1.84.1-src.tar.gz` 完成。旁注:这大约比在同一台机器上构建整个 OCaml 工具链快一分钟。解压后的源码树现在为 1.9 GB。这已经超过了地球上活着印度人的字节数(译者注:一种夸张说法)。 让我们继续,再次打开 `README.md`,这次是从我们刚刚解压的源码树中。它仍然写着 *“从源码构建 [...] 不推荐 [...] 请参阅 INSTALL.md”*,但遗憾的是,它引用的那个 `INSTALL.md` 并没有包含在 tarball 中。好吧,我们已经有了 1.9 GB 的源码,似乎没有足够的空间来包含安装文档 —— 优先级啊啊啊!我们已经告诉过你,从源码构建不推荐! ## 统计代码行数 在我们开始构建 Rust 之前(我想拖延这件事),让我们“快速”在 Rust 源码树上运行 `cloc`…… 快速的定义是: ……它还在统计文件数…… ……还在统计……啊……还在统计…… ……一分多钟过去了,显示 351365 个文本文件…… ……现在似乎在统计唯一文件…… ……又过了两分钟,显示 276326 个唯一文件……哇…… ……它还在统计,不管在统计什么,这次可能是行数…… ……天哪,CPU 风扇全速运转,已经 7 分钟了…… ……公平地说,Perl(`cloc`)可能不是这里的最佳选择…… ……我们需要用 Rust 实现的 `tokei`…… ……8 分钟过去了…… ……如果这还要更久,我可能得插上电源…… ……9 分钟(或者 3 倍 OCaml 时间)……然后 3-2-1…… ……最后一次尝试……10 分钟,我们完成了! 那真是相当快,不是吗?除了得到一堆 “行数统计超时:” 的行和 `81443 个文件被忽略` 之外,`cloc` 的输出确实令人印象深刻: | 文件 | 语言 | 空白行 | 注释 | 代码 | |------|------|--------|------|------| | 68005 | Rust | 1251355 | 2413958 | 20734039 | | 51146 | C++ | 1435178 | 2396405 | 7685767 | | 71995 | C | 1083792 | 2936843 | 5724464 | | ... | ... | ... | ... | ... | | 19 | Pest | 226 | 28 | 984 | | ... | ... | ... | ... | ... | | 1 | Brainfuck | 3 | 4 | 10 | | 1 | sed | 0 | 0 | 5 | | 276326 | SUM | 6345959 | 10746776 | 50771605 | 一个真正引人注目的语言列表!我不得不从输出中裁剪掉 101 行以使其适合。什么是“Pest”和“SnakeMake”???哈哈,至少有 10 行 Brainfuck! 好了,这总共是**两千万行 Rust 代码**,不包括注释和空行,分布在 68005 个 Rust 文件中,对吧?总共有 5000 万行“代码”在 276326 个文件中。令人印象深刻!而且这个列表甚至不完整,因为它遇到了几次“超时”。 作为参考,让我们“快速”——3 秒钟——统计 OCaml 5.4.0 工具链的代码行数: | 文件 | 语言 | 空白行 | 注释 | 代码 | |------|------|--------|------|------| | 2825 | OCaml | 57092 | 106578 | 366954 | | 307 | C | 8478 | 10140 | 47737 | | 770 | Bourne Shell | 5718 | 62233 | 52161 | | 2 | m4 | 148 | 11 | 108 | | 1234 | C/C++ Header | 1747 | 3203 | 5084 | | 12 | Assembly | 548 | 216 | 948 | | 28 | make | 225 | 916 | 576 | | 346 | Markdown | 622 | 341 | 849 | | 12 | AsciiDoc | 518 | 0 | 168 | | ... | ... | ... | ... | ... | | 1 | C# | 20 | 9 | 34 | | 33 | SUM | 78065 | 130234 | 487018 | (从输出中删除 19 行) 总共是**50 万行代码**,没有“Pest”,没有 SnakeMake,零行 Brainfuck。够糟了,里面还保留了 9 行 C#,那肯定是错误 :)。 对于像 OCaml 这样的高级语言来说,50 万行代码并不算太糟,它附带了一个字节码解释器和针对 5 个平台(x86-64, arm64, RISC-V, s390x 和 powerpc)的原生代码生成器。此外,值得注意的是,OCaml 只需一个 C 编译器和标准构建工具(如 `gmake`、`m4` 等)即可开箱即用。 ## 构建 Rust 现在让我们编译 Rust!由于 Rust 1.84.1 未能构建(大约两个小时后出现了某种错误信息,如果我没记错的话),我尝试构建 Rust 1.81 代替。 以下是 Rust 1.81 的构建时间。请坐稳: **12563 秒**,或者**3 小时 30 分钟**。 OCaml 在 197 秒内构建完成,即 3 分 17 秒。显然是在同一台机器上。 这比 OCaml 慢了 63 倍,比 Python 慢了 162 倍,比 Lua 慢了 4753 倍。 公平地说,Rust 的构建时间包括了构建 LLVM、cargo 和其他一些工具,并且它至少构建了 Rust 两次:`stage1` 是用 Rust 1.80 引导程序构建的 Rust 1.81 编译器,而 `stage2` 使用 `stage1`(1.81)再次构建自身。 对于 DragonFly BSD,我们使用我们在 github 上的自己的引导仓库 (https://github.com/DragonFlyBSD/rust-bootstrap-dragonfly)。如果你有适当版本的 `cargo`、引导 `rustc` 和 Python 安装,那么以下命令可能(也可能不)能引导 Rust: ```bash export LIBSSH2_NO_PKG_CONFIG=1 export LIBGIT2_NO_PKG_CONFIG=1 export LIBCURL_NO_PKG_CONFIG=1 export LIBZ_NO_PKG_CONFIG=1 export LIBLZMA_NO_PKG_CONFIG=1 export PROFILE=release export LIBZ_SYS_STATIC=1 export OPENSSL_NO_PKG_CONFIG=1 ./configure \ --release-channel=stable \ --enable-cargo-native-static \ --enable-extended \ --enable-vendor \ --enable-locked-deps \ --local-rust-root=/path/to/bootstrap/compiler \ --sysconfdir=/opt/rust/etc \ --prefix=/opt/rust \ --python=python \ --disable-llvm-static-stdcpp \ --disable-docs python x.py build --config ./config.toml python x.py dist --config ./config.toml python x.py install --config ./config.toml ``` ## 先有鸡还是先有蛋? 不仅构建需要 3 小时 30 分钟,还有另一个问题: 对于 Rust,目前没有用除 Rust 之外的其他语言编写的引导编译器,所以你需要有一个 Rust 编译器……来编译 Rust 编译器……而你需要有一个 Rust 编译器……来编译 Rust 编译器……而你需要有一个 Rust 编译器……来……*此时栈溢出发生,停止了我们优美的无限递归*。 公平地说,这并非真正的无限递归,因为曾经有一个**用 OCaml 编写的 Rust 编译器**,但那已经是十多年前的事了,早在我 2013 年开始使用 Rust 之前。 想象一下,你确实想从早期版本的 Rust 开始,那时编译器是用 OCaml 编写的,然后逐个版本编译每个 Rust,直到达到当前版本。理论上可行。我粗略估计,你将不得不编译大约一百个中间的 Rust 编译器,这可能会让一台强大的构建机器 24/7 忙碌 10 天或更长时间,还不包括修复错误和在失败尝试后应用补丁。不切实际。 ## Rust 的 n+1 问题 请注意,问题的重要组成部分,以及引导 Rust 如此昂贵的原因,在于每个版本的 Rust 必须用恰好前一个版本来引导。 也许现在不再是这种情况,但过去是这样。为了构建 Rust 版本 n+1,你需要 Rust 版本 n。 此时,看看其他语言如何进行引导过程可能会有所帮助。 ## 其他语言如何引导自身 *Zig 语言*以某种方式提供了一个**130 万行的 ANSI C 文件** `zig1.c`,其中包含了 Zig 编译器和 LLVM。我认为,他们现在将用 Zig 编写的 Zig 编译器编译成 WASM,然后从 WASM 编译成 C。聪明。只是不要试图在 `vim` 中打开那个文件。一旦他们摆脱 LLVM,很多臃肿将会消失。 另一方面,*OCaml* 附带了一个用 C 实现的字节码解释器(`ocamlrun`)。这使他们能够提供一个 3.4M 大小的可移植引导编译器作为字节码二进制文件(参见 `boot/ocamlc`),然后用它来引导 OCaml 编译器。这个过程在 OCaml 的 `BOOTSTRAP.adoc` 中有详细说明。 *Go 语言*的编译器也是用 Go 自身编写的,但 Go 对引导另一个 Go 编译器所需的 Go 编译器版本不那么严格。例如,Go 1.24 和 1.25 需要一个 1.22 版本的 Go 编译器。此外,Go 1.4 是用 C 编写的,你可以用它们来引导更新的 Go 编译器。更重要的是,Go 编译大约需要 3 分钟。这使得在出现问题时更容易修复。 *Python、Ruby 或 Lua* 等语言都是用 C 实现的——它们不需要引导自身。 *Erlang/OTP* 运行时系统 `erts` 是用 C/C++ 实现的,并使用字节码解释器(和 JIT)来运行 BEAM 字节码。当你下载 Erlang/OTP 的源码时,它包含了 Erlang 编译器的预编译字节码以及引导 Erlang 所需的一切。 *Elixir*,一种运行在 Erlang/OTP 平台上的语言,则略有不同。它的编译器是用 Erlang 而不是 Elixir 实现的。只需安装 Erlang/OTP,你就可以引导 Elixir。 ## 在没有二进制文件的情况下引导 Rust 但是,如果由于某些原因,你的平台没有可用的二进制 Rust 引导编译器呢? 那么,你必须使用在另一个系统上运行的现有二进制引导编译器来交叉编译 Rust 编译器 `rustc`。我做过一次,大约十年前,为了 DragonFly。这并不有趣。 这里需要注意的是,你仍然需要另一个平台的二进制引导编译器来为你的系统交叉编译 Rust。目前,没有可行的方法绕过这一点。 ## 没有二进制引导,就没有 Rust 总结一下,据我所知,如果不下载现有的二进制 Rust 引导编译器,几乎不可能编译官方的 Rust 发行版。我很乐意听到相反的说法。 就个人而言,我对此并不太担心,但如果我是来自俄罗斯、中国、朝鲜或伊朗的软件开发人员,我会稍微担心需要从远在美国的服务器下载二进制 Rust 编译器。能出什么问题呢? ## 结论 —— Rust 很臃肿 就大小而言,1.84.1 版本的“官方”Rust 实现相当“臃肿”: - 两千万行 Rust 源码(总计五千万行) - 1.9 GB 源码(未压缩),4 GB 已安装二进制文件 - 构建需要 3 小时 30 分钟 与 OCaml 5.4.0 版本相比: - 50 万行代码(总计) - 31 MB 源码(未压缩),300 MB 已安装二进制文件 - 构建需要 3 分 17 秒 此外,没有二进制引导编译器你就无法构建 Rust。我希望一旦 GCC Rust 前端 (https://github.com/Rust-GCC/gccrs) 变得更加成熟,这种情况会有所改变。 在我看来,*二进制引导问题* 和 Rust 依赖的*大量依赖* 使得 Rust 不是系统编程任务的首选,因为理想情况下你希望依赖尽可能少。如果你正在开发基础工具,其他更复杂的东西将构建在这些工具之上,那么基础不应该比构建在其上的东西复杂一千倍。你同意吗? 如果你正在构建大型企业应用或专有的、安全关键的嵌入式产品,问题就不那么严重了。如果你在这个领域,要注意 Rust crate 的供应链攻击,永远不要盲目执行 `cargo update`!祝你好运追踪你的软件可能依赖的数百个 crate 的更改。 就个人而言,我并不完全信服下载一个 699 MB 的压缩 tarball,解压成 1.9 GB,然后花*三个半小时*编译 Rust,仅仅为了使用: - 最新最好的控制台文本编辑器 - 一个极快的 Python 包管理器 - 一个内存安全的 `cat` 或其他“核心工具” - Tailwind CSS - 下一级 Ruby JIT 我的建议是:不要仅仅因为 Rust 当前流行就盲目地用它做所有事情。仔细思考 Rust 是否真的是完成手头任务的正确工具。有许多好的替代方案: - Go (https://go.dev/) “简单”,开箱即用提供世界级的交叉编译和静态编译二进制文件(它曾经救过我) - OCaml (https://ocaml.org/) 是一种优秀的系统编程语言。它很大程度上遵循 UNIX 哲学*和*函数式编程范式,并且易于设置。有些人用它构建类型安全的 unikernel (https://mirage.io/) - 真正酷的孩子们 (https://tigerbeetle.com/) 使用 Zig (https://ziglang.org/) - 对于小任务,为什么不使用通用语言 C? - Hare (https://harelang.org/) 是一种简单、稳定、健壮的系统编程语言 晚安。

相似文章

Rust语言的性能

Lobsters Hottest

本次演讲分析了Rust相较于C++的性能优势与劣势,提供了基准测试和最佳实践。附有幻灯片和阅读材料。

Bun 的问题可能在于公开开发

Lobsters Hottest

一篇分析 Bun 实验性使用 LLM 将其 Zig 代码库转译到 Rust 所引发的争议的文章,强调公众的强烈反应源于透明的开发实践而非实验本身。