用 cargo-llvm-lines 定位泛型膨胀:从 3.4 万行 IR 砍到 9 千

场景很具体:Rust 1.78 stable,x86_64-unknown-linux-gnu,一个内部序列化库,产物 release + lto=thin。业务侧反馈编译要 4 分半,我拿 cargo-llvm-lines 量了一下,最热的单态化函数一个就有 3.4 万行 LLVM IR,光这一个符号占全 crate 的 18%。

做法分三步。先装工具,cargo install cargo-llvm-lines,注意它统计的是 cargo rustc -- -Z... 之外的那条路径,所以环境变量得手动带上:

CARGO_PROFILE_RELEASE_LTO=thin \
cargo llvm-lines --release -p my-serde-lib | head -40

输出按 IR 行数排序,重点看 core::fmt::write、serde::ser::Serialize::serialize 这类泛型入口。第二步是找到膨胀源头:我的 Serialize 实现里对 Vec<T> 和 HashMap<K,V> 各写了一个手写 impl,每个 T 都单独实例化一遍。改成在序列化入口处走 &dyn Serialize:

fn write_all(items: &[Box<dyn Serialize>]) -> Result<(), Error> {
    for it in items { it.serialize(&mut ser)?; }
    Ok(())
}

第三步,把内联门槛调低一点,#[inline(never)] 打在几个确实不值得内联的包装函数上,再跑一遍 cargo llvm-lines 对比。IR 从 3.4 万行降到 9 千,编译时间从 4 分半到 2 分 40 秒。

为什么有效:单态化是按类型生成代码,Vec<A>、Vec<B>、Vec<C> 是三份独立机器码;dyn 只在虚表调用处多一次间接跳转,代价是一次指针解引用,换来代码量线性下降。代价也明确:热路径上的虚调用会让 LLVM 失去跨函数优化窗口,我的序列化本来就不是热点,所以能接受;如果是解码器核心循环,这个改法会掉性能。

会失效的地方:开了 lto=fat 之后 cargo llvm-lines 的数字会失真,它统计的是单 crate 的 IR,链接期还会合并;#[inline(always)] 的泛型函数它照单全收,不代表真膨胀;另外 workspace 里要带 -p,不然会把测试 target 也算进去,数字虚高。我踩过的坑是第一次忘了带 profile 环境变量,量的是 debug 的 IR,白忙活一下午。

你们项目里有没有那种「一个泛型 impl 撑起半个 crate IR」的函数?最后是改 dyn 还是拆成非泛型内核?

0
赞同度 100% · 0 条评论 · 2 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。