1210 字
6 分钟
Rust Docker 编译时间优化
2025-07-01
无标签

问题背景#

原文:https://sharnoff.io/blog/why-rust-compiler-slow

原作者在将个人网站从传统部署方式迁移到容器化部署时,遇到了一个让人头疼的问题:每次代码变更后,Docker构建都需要将近4分钟时间,其中大部分时间都花在了Rust项目的编译上。

初始的简单Dockerfile构建时间分布:

  • 下载依赖:10秒
  • 编译项目:3分51秒

这对于快速迭代开发来说完全无法接受。

第一步:使用cargo-chef优化依赖缓存#

首先尝试使用cargo-chef来缓存依赖构建,这是Docker Rust构建的标准优化方案:

FROM rust:1.87-alpine3.22 AS planner
COPY . .
RUN cargo chef prepare --recipe-path=/workdir/recipe.json

FROM rust:1.87-alpine3.22 AS cooker
COPY /workdir/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path=/workdir/recipe.json

FROM cooker AS builder
COPY . .
RUN cargo build --release --package web-http-server

然而,优化效果令人失望:

  • 依赖编译:1分7秒
  • 最终binary编译:2分50秒

问题发现:75%的编译时间仍然花在最终binary上,而不是依赖上!

深入分析:rustc到底在做什么?#

使用cargo —timings#

cargo build --release --timings

这个命令生成了详细的时间报告,但对于单一crate的编译,信息有限。唯一的收获是确认了174.1秒的精确编译时间。

使用rustc自分析功能#

RUSTC_BOOTSTRAP=1 RUSTFLAGS='-Zself-profile' cargo build --release

通过measureme工具分析结果:

$ summarize summarize web_http_server.mm_profdata | head
+-------------------------------+-----------+-----------------+----------+------------+
| Item                          | Self time | % of total time | Time     | Item count |
+-------------------------------+-----------+-----------------+----------+------------+
| LLVM_lto_optimize            | 851.95s   | 33.389          | 851.95s  | 1137       |
| LLVM_module_codegen_emit_obj | 674.94s   | 26.452          | 674.94s  | 1137       |
| LLVM_thin_lto_import         | 317.75s   | 12.453          | 317.75s  | 1137       |
| LLVM_module_optimize         | 189.00s   | 7.407           | 189.00s  | 17         |

重大发现:80%的时间都花在了Link Time Optimization (LTO)上!

LTO性能调优#

理解LTO设置#

作者的配置中设置了lto = "thin",这是问题的根源。测试不同LTO和debug设置的组合:

LTO设置debug=nonedebug=full
禁用LTO50.0s / 21.0Mi67.6s / 214.3Mi
Thin local LTO67.5s / 20.1Mi88.2s / 256.8Mi
”Thin” LTO133.7s / 20.3Mi172.2s / 197.5Mi
”Fat” LTO189.1s / 15.9Mi287.1s / 155.9Mi

关键洞察

  • 完全禁用LTO和debug符号后,编译时间降至50秒
  • Fat LTO比禁用LTO慢4倍
  • Debug符号增加30-50%编译时间

优化级别调优#

既然依赖已经缓存,可以对最终binary和依赖设置不同的优化级别:

[profile.release]
lto = "off"
debug = "none"
opt-level = 0  # 最终binary禁用优化

[profile.release.package."*"]
opt-level = 3  # 依赖使用高优化级别

测试结果显示:

  • opt-level = 0:14.7秒(但性能损失大)
  • opt-level = 1:48.8秒(较好的平衡)
  • opt-level = 2/3:~51秒

深入LLVM层面分析#

使用LLVM时间跟踪#

RUSTFLAGS='-Zllvm-time-trace' cargo build --release

生成了1.4GB的Chrome tracing格式文件!虽然现有的可视化工具都无法处理如此大的文件,但可以通过命令行分析:

# 转换为每行一个事件的格式
cat web_http_server.llvm_timings.json \
| sed -E 's/},/}\n/g;s/^\{"traceEvents":\[//g;s/\],"beginningOfTime":[0-9]+}$//g' \
> web-http-server.llvm_timings.jsonl

分析最耗时的操作#

$ cat web_http_server.llvm_timings.jsonl | jq -r 'select(.name | startswith("Total ")) | "\(.dur / 1e6) \(.name)"' | sort -rn | head
665.369662 Total ModuleInlinerWrapperPass
656.465446 Total ModuleToPostOrderCGSCCPassAdaptor
632.441396 Total DevirtSCCRepeatedPass
182.250077 Total InlinerPass
189.621119 Total OptFunction

发现两个主要瓶颈:

  1. 内联优化 (InlinerPass)
  2. 函数优化 (OptFunction)

针对性优化#

调节内联阈值#

LLVM提供了内联相关的参数:

RUSTFLAGS="-Cllvm-args=-inline-threshold=50 -Cllvm-args=-inlinedefault-threshold=50 -Cllvm-args=-inlinehint-threshold=50"

将内联阈值从225降到10,编译时间从48.8秒减少到42.2秒。

分析函数级别优化#

使用更新的symbol mangling v0格式获得更详细的函数信息:

RUSTFLAGS="-Csymbol-mangling-version=v0"

发现最耗时的函数优化:

  • web_http_server::photos::PhotosState::new::{closure#0}: 1.99秒
  • web_http_server::run::{closure#0}: 1.56秒

async函数的问题#

重要发现:rustc内部将async函数表示为闭包,导致符号显示为{closure#0}。大型async函数特别难以优化。

解决方案

  1. 拆分大型async函数
  2. 将复杂的Future包装为Pin<Box<dyn Future>>以简化类型

通过这种方法,PhotosState::new的优化时间从5.3秒减少到2.14秒。

最终优化结果#

采用综合优化策略:

RUN RUSTFLAGS='-Cllvm-args=-inline-threshold=10 -Cllvm-args=-inlinedefault-threshold=10 -Cllvm-args=-inlinehint-threshold=10' \
    cargo build --timings --release --target=x86_64-unknown-linux-musl --package web-http-server

优化历程

  • 初始状态:~175秒
  • 禁用LTO和debug符号:51秒 (-71%)
  • 调整opt-level到1:48.8秒 (-4%)
  • 减少内联:40.7秒 (-16%)
  • 本地代码优化:37.7秒 (-7%)
  • 依赖项优化:32.3秒 (-14%)

最终结果:从175秒优化到32.3秒,提升81.5%!

关键教训#

  1. LTO是双刃剑:虽然能提升运行时性能,但严重影响编译速度
  2. async函数复杂性:大型async函数会生成复杂的状态机,LLVM优化困难
  3. 工具链的力量:rustc的self-profiling和LLVM的时间跟踪提供了宝贵的性能洞察
  4. 渐进式优化:通过系统性分析,找到真正的瓶颈而非盲目优化

对Rust生态的思考#

这次深度分析暴露了几个值得关注的问题:

  1. async函数编译性能需要改进
  2. core::ptr::drop_in_place<T>可能应该在定义T的crate中编译
  3. 需要更好的工具来帮助开发者识别编译瓶颈
Rust Docker 编译时间优化
https://blog.lpkt.cn/posts/rust-docker-compile-time-opt/
作者
lollipopkit
发布于
2025-07-01
许可协议
CC BY-NC-SA 4.0