Rust 的 derive 竟隐含 inline 陷阱

Rust's derive often implies inline

在 Rust 开发中,我们习惯使用 #[derive(...)] 来快速实现 Debug、Clone 等核心特性。最近我发现了一个容易被忽视的细节:Rust 编译器在展开这些 derive 宏时,往往会自动添加 #[inline] 属性。对于简单的结构体,内联能提升性能,但在处理深层嵌套的错误层级(如 ErrorA、ErrorB、ErrorC)时,这种自动内联会导致代码膨胀。我们在优化 uv 项目时发现,通过自定义 proc macro 禁止特定 Debug 实现的内联,成功将二进制文件体积缩小了约 160KB。这提醒我们,虽然 #[inline] 只是提示,但在复杂场景下,盲目依赖 derive 的默认行为可能会付出意想不到的二进制体积代价。

Rust 编译器似乎没有对 Debug 实现的内联大小或次数施加任何限制,这导致生成的代码量可能远超预期。
  1. vlovich123

    我觉得 Debug 特性应该从一开始就整体采用惰性生成——仅仅作为一个永远不会展开的特殊标记。毕竟 99% 的 Debug 实现根本不会被用到,而把剩下的标记为 #[cold] 并内联显然是错误的。当然,在实践中实现这一点听起来异常困难。

    话说回来,即便是那个旨在提升性能的 PR 本身,也是性能提升与退步的混合体。

  2. Sharlin

    我相当确信 Debug 绝不应该被内联。Display 大概也不该内联,fmt 机制本身已经够重了,即使是在序列化密集型的工作负载中,不内联恐怕也不是瓶颈。我自己曾不得不将一些 Debug/Display 实现标记为 #[inline(never)],这让二进制文件缩小了几十 KB(在几百 KB 的总量中,这算是相当显著的缩减)。

  3. scottlamb

    我好奇他们是否曾考虑过为 `#[derive(Debug)]` 采用一种基于表驱动的 approach,就像 `facet` [1] 做的那样。对于这种公式化程度很高、且二进制大小和编译时间比执行速度更重要的场景,这本来是我的第一直觉。不过我的印象是 facet 在这些方面尚未完全兑现其承诺,所以也许 std 中类似的表驱动方案也曾被尝试过但最终被否决了。

    [1] https://crates.io/crates/facet

同日更多故事

2026-10-07