Cargo 编译性能优化实战:profile 分析 + 依赖裁剪 + 并行加速
2026/7/14 14:45:53 网站建设 项目流程

Cargo 编译性能优化实战:profile 分析 + 依赖裁剪 + 并行加速

作为一个自学 Rust 的人,我最大的痛点就是编译慢。第一次编译一个中型项目,等了 8 分钟,我甚至怀疑是不是电脑有问题。后来才知道,Cargo 的编译慢不是偶然的——Rust 的类型系统、宏展开、codegen 单元都比其他语言更重。但这不代表没法优化。这篇就把我从 8 分钟编译优化到 2 分钟的实战过程完整写出来。

一、为什么 Rust 编译慢?——一个自学者的"朴素拆解"

Rust 编译慢有几个根本原因:

  1. 宏展开derive(Deserialize)这种宏会在编译时生成大量代码,展开后的代码量远超你写的。
  2. 类型推导 + 单态化:泛型函数对不同类型生成不同版本,比如Vec<i32>Vec<String>各有一份代码。serde的泛型特别多,单态化后的代码膨胀很严重。
  3. LLVM 后端:Rust 用 LLVM 做 codegen,LLVM 对大量函数做优化很耗时。特别是 debug 模式下也会跑 LLVM 优化(虽然级别低)。

我第一次 profile 编译时间时,发现 serde 相关的 crate 占了总编译时间的 40%。这让我意识到:编译优化不是"加快编译器",而是"减少编译器要干的事"。

flowchart TD A["Cargo 编译慢"] --> B["宏展开<br/>derive 大量生成代码"] A --> C["单态化<br/>泛型膨胀"] A --> D["LLVM codegen<br/>优化耗时"] B --> E["编译时间占比"] C --> E D --> E E --> F["serde: 40%"] E --> G["tokio: 15%"] E --> H["项目代码: 20%"] E --> I["其他依赖: 25%"] style F fill:#f66 style G fill:#fc6

二、profile 分析:找出编译时间的真正瓶颈

Cargo 自带编译时间统计,但默认不开启。加上-Z timings标志(需要 nightly)可以看到每个 crate 的编译时间分布:

# 编译时输出每个 crate 的时间统计 cargo +nightly build -Z timings # 生成的 HTML 报告会保存到 target/cargo-timings/ # 打开后可以看到: # - 每个 crate 的编译时间 # - 哪些 crate 之间有依赖关系(影响并行度) # - codegen 单元的数量和耗时

稳定版 Rust 也有一个实用的 flag:--timings

# 稳定版 Rust 的编译时间统计 cargo build --timings # 会在 target/cargo-timings/ 生成报告

另外,cargo build --verbose也能看出编译顺序:

# 查看编译顺序和每个 crate 的处理时间 cargo build --verbose 2>&1 | grep "Compiling" # 输出类似: # Compiling serde v1.0.193 → 12.3s # Compiling serde_derive v1.0.193 → 8.7s # Compiling tokio v1.35.1 → 6.1s # Compiling my-project v0.1.0 → 3.2s

我还用sccache来缓存编译结果,特别是依赖 crate 的编译。第一次编译后,后续只要依赖版本不变,sccache 会直接用缓存:

# 安装 sccache cargo install sccache # 设置环境变量让 Cargo 使用 sccache export RUSTC_WRAPPER=sccache # 第一次编译会正常慢,但后续编译会快很多 cargo build # 查看缓存统计 sccache --show-stats # 输出类似: # Cache hits: 85/100 → 85% 缓存命中! # Cache misses: 15/100
flowchart LR subgraph "无 sccache" A1["每次全量编译<br/>依赖也要重编"] --> A2["编译 8 分钟"] end subgraph "有 sccache" B1["依赖缓存命中 85%<br/>只编译项目代码"] --> B2["编译 2 分钟"] end A1 -->|安装 sccache| B1 style A2 fill:#f66 style B2 fill:#6f6

三、依赖裁剪:砍掉没用到的功能和 crate

Cargo 的features机制是依赖裁剪的关键。很多 crate 默认开启一堆 features,你可能只用了一小部分。比如 serde 默认开启stdderive,但如果你只在no_std环境用,stdfeature 就是多余的。

实际操作:

# Cargo.toml 优化前后对比 # ❌ 优化前:默认 features 全开 [dependencies] serde = "1.0" tokio = { version = "1", features = ["full"] } # full 包含所有 features! reqwest = "0.11" # 默认开启很多 features # ✅ 优化后:只启用需要的 features [dependencies] serde = { version = "1.0", default-features = false, features = ["derive"] } # 只用 derive,不需要 std(如果项目已经依赖 std) tokio = { version = "1", features = ["rt", "macros", "net"] } # 只用 runtime + macros + net,不需要 fs/process/io-util/signal/tracing 等 reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } # 只用 json 解析 + rustls TLS,不用 default-tls(openssl) 和 cookies 等

我还用cargo-udeps检查未使用的依赖:

# 安装 cargo-udeps(需要 nightly) cargo install cargo-udeps # 检查哪些依赖没有被代码实际引用 cargo +nightly udeps # 输出类似: # warning: unused dependency: "tempfile" (specified in Cargo.toml but never imported) # warning: unused dependency: "chrono" (specified but only used in tests)

裁剪依赖的实际效果很显著。我的项目优化前有 47 个依赖 crate,裁剪后变成 23 个。编译时间从 8 分钟降到 5 分钟(因为少了近一半的 crate 要编译)。

// ❌ 错误:引用了整个 crate 但只用了很小一部分 use itertools; // 整个 itertools crate,很多功能没用到 // ✅ 正确:只引入需要的函数,或者干脆不用重依赖 // 用标准库替代 itertools 的常用功能 fn flatten_nested<T>(nested: Vec<Vec<T>>) -> Vec<T> { nested.into_iter().flatten().collect() // 标准库的 flatten 就够了 } fn zip_with_index<T>(items: Vec<T>) -> Vec<(usize, T)> { items.into_iter().enumerate().collect() // 标准库的 enumerate 就够了 }
graph TD A["项目依赖 47 个 crate"] --> B["cargo-udeps 检查"] B --> C["移除 12 个未使用依赖"] B --> D["裁剪 12 个 crate 的多余 features"] C --> E["剩余 23 个精简依赖"] D --> E E --> F["编译时间: 8分钟 → 5分钟"] style C fill:#6f6 style D fill:#6cf style F fill:#6f6

四、并行加速:codegen-unit 和 cargo 的并行编译

Cargo 默认会并行编译不同的 crate(没有依赖关系的 crate 可以同时编译)。但单个 crate 内部的编译是串行的——LLVM 处理一个 crate 的所有函数,没法拆成多线程。

Rust 提供了codegen-units参数来解决这个问题:把一个 crate 的代码拆成多个 unit,每个 unit 独立编译,可以并行。代价是:拆 unit 会降低 LLVM 的跨函数优化效果。所以 release 模式默认codegen-units = 1(不拆,最大化优化),debug 模式默认codegen-units = 16(拆成16份,加快编译)。

# Cargo.toml profile 配置 # release 模式:编译速度 vs 运行性能的权衡 [profile.release] # 如果追求最快运行性能:codegen-units = 1(默认) # 如果追求最快编译速度:codegen-units = 16(牺牲约 5-10% 运行性能) codegen-units = 4 # 折中:4份并行,编译快一些,性能损失约2-3% # debug 模式默认就是 16,不用改 # 本地开发用的 release-like profile:编译快,性能也还行 [profile.fast-release] inherits = "release" # 继承 release 配置 codegen-units = 16 # 最大并行 lto = false # 不做跨 crate LTO(大幅加快编译) opt-level = 2 # 优化级别 2(比 3 稍慢运行,但编译快很多) # 生产发布用的正式 profile:运行最快 [profile.production] inherits = "release" codegen-units = 1 # 不拆,LLVM 全局优化 lto = "thin" # thin LTO:比 full LTO 快,效果也不错 opt-level = 3 # 最高优化级别

使用不同 profile 编译:

# 开发阶段用 fast-release(编译快) cargo build --profile fast-release # 生产发布用 production(运行快) cargo build --profile production

实测效果:

Profile编译时间运行性能适用场景
dev (默认)8min基准本地调试
fast-release2min比dev快约3x开发迭代
release (默认)5min最快生产发布
production7min比release快约5%极致性能

thin LTO 的编译时间比 full LTO 短很多,效果也接近。我推荐开发阶段不用 LTO,发布阶段用 thin LTO。

graph LR subgraph "codegen-units = 1" A1["LLVM 全局优化<br/>编译慢(5min)<br/>运行快"] end subgraph "codegen-units = 4" A2["4份并行编译<br/>编译中(3min)<br/>运行稍慢2-3%"] end subgraph "codegen-units = 16" A3["16份并行编译<br/>编译快(2min)<br/>运行慢5-10%"] end A1 -->|开发阶段| A3 A3 -->|发布阶段| A1 style A1 fill:#6f6 style A3 fill:#6cf

五、总结

Cargo 编译优化不是什么神秘技巧,核心思路就是"减少编译器要干的事"。我从 8 分钟降到 2 分钟,做了四件事:

  1. sccache 缓存:依赖不变时缓存命中 85%,省了最大的编译时间。安装和配置只需要两行命令。
  2. 依赖裁剪:用cargo-udeps找出未使用依赖,用default-features = false只启用需要的 features。crate 数量从 47 降到 23。
  3. codegen-units 调整:开发阶段设为 16(最快编译),发布阶段设为 1(最快运行)。折中设为 4。
  4. LTO 策略:开发阶段不做 LTO,发布阶段用 thin LTO(比 full LTO 快得多,效果接近)。

我的建议顺序是:先装 sccache(最小成本最大收益),再裁剪依赖(收益稳定),最后调 profile(需要权衡编译速度和运行性能)。别像我一开始那样,看到编译慢就去调 profile,结果调了半天发现瓶颈在依赖上——sccache + 裁剪依赖才是真正的"大头"。

作为的 Rust 学习者,我最大的教训是:编译慢不是你代码的问题,是依赖和配置的问题。先 profile 再优化,不要凭感觉乱调参数。cargo build --timingscargo udeps这两个工具,是我优化编译时间的"照明灯"——没有它们,我就是在黑暗中摸索。

完整配置和测试脚本在 GitHub,有问题评论区讨论。编译慢是 Rust 的"门槛",但理解了原理之后,优化起来并不难。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询