首页 [译] Rust 中的内联
文章
取消

[译] Rust 中的内联

免责声明

本文是对原博文《Inline In Rust》的无授权翻译转载,不享受任何著作权利,不用于任何商业目的,不以任何许可证进行授权,不对任何转载行为尤其是商业转载行为负责。一切权利均由原作者 Aleksey Kladov 保有。

本文中出现的所有第一人称均指代 Aleksey Kladov 而非译者本人。本文中对一些术语会额外附加英文原文注释,以帮助读者搜索相关概念。

前言

在 Rust 中有许多关于 #[inline] 属性的部落知识(Tribal Knowledge)。我发现我经常教别人它是如何工作的,所以我最终决定将它写下来。

读者留心:这是我所知道的,并不一定是对的。此外,#[inline] 的确切语义并非一成不变,也许在未来的 Rust 版本中会发生变化。

为什么内联很重要?

内联是一种优化转换,它使用函数体替换函数调用。

举一个简单的例子,在编译期间,编译器可以将这段代码:

1
2
3
4
5
6
7
fn f(w: u32) -> u32 {
    inline_me(w, 2)
}

fn inline_me(x: u32, y: u32) -> u32 {
    x * y
}

转换为这样:

1
2
3
fn f(w: u32) -> u32 {
    w * 2
}

Frances AllenJohn Cocke 的《A Catalogue of Optimizing Transformations》来解释:

内联有很多明显的优势,其中两个:

  1. 没有任何函数调用开销
  2. 调用者和被调用者一起被优化。可以利用特定的参数值和关系:常量参数可以折叠到代码中,被调用者中的不变指令可以移动到调用者的不经常执行的区域,等等。

换而言之,提前编译好的语言内联乃是其余所有优化之母。它为编译器提供了必要的上下文以应用进一步的转换。

内联和分离式编译

内联与编译器中的另一个重要思想——分离式编译(Separate Compilation)相冲突。编译大型项目时,最好将它们拆分为可以独立编译的模块,以:

  • 并行处理所有内容
  • 对单个改变的模块进行范围增量重编译(Scope Incremental Recompilation)

为了实现分离式编译,编译器暴露函数签名,但是保持函数体对其他模块不可见,阻止内联。这种基本矛盾使得 Rust 中 #[inline],而不单单是编译器内联函数的提示变得更加棘手。

Rust 中的内联

Rust 中,一个独立的(分离式)编译单元是 crate。如果在 crate A 中定义了函数 f,则 A 中所有对 f 的调用都可以被内联,因为编译器可以完整访问 f。但是,如果从某个下游 crate B 调用 f,则此调用不能被内联。B 只能访问 f 的签名而不是它的函数体。

这就是 #[inline] 主要用法的来源——它支持跨 crate 内联。没有 #[inline],即使是最微不足道的函数也不能跨过 crate 的边界被内联。好处并非没有成本——编译器通过为每一个调用 #[inline] 函数的 crate 都编译一份它的拷贝来实现上述功能,显著增加了编译时间。

除了 #[inline],还有两个例外。泛型函数是隐式可内联的。实际上,编译器只有在知道实例化的特定类型参数时才能编译泛型函数。正如大家所知道的那样,在被调用的 crate 中,泛型函数的函数体必须始终可用。

另一个例外是链接时优化(LTO, Link-Time Optimization)。LTO 选择不参与分离式编译——它使所有函数的函数体都可用,但代价是编译速度慢得多。

实践中的内联

既然解释了底层语义,就可以推断出一些使用 #[inline] 的准则。

首先,不管三七二十一地使用 #[inline] 绝非良策,因为这会使得编译时间更加糟糕。如果您不关心编译时间,一个更好的办法是在 Cargo 配置文件中设置 lto = true

其次,通常不需要为私有函数应用 #[inline]——在一个 crate 内部,编译器通常会做出更好的内联策略。有一个笑话说的是 LLVM 对于何时应该内联函数的捷思法(Heuristic)是“是”。

第三,在构建应用时,当分析显示某个特定的小函数是瓶颈时才被动地使用 #[inline]。考虑在发布时使用 lto。主动 #[inline] 不重要的公有函数可能是有意义的。

第四,在构建库时,主动给小型的非泛型函数添加 #[inline]。特别注意实现 DerefAsRef 类似的东西经常受益于内联。库无法预知所有的使用,不要过早地对未来的使用者抱有悲观态度才是正确的。请注意 #[inline] 不具有传递性:如果一个不重要的公有函数调用了一个不重要的私有函数,你应该同时 #[inline] 二者。参阅此基准(Benchmark)以了解更多。

第五,考虑通用函数。说泛型函数是隐式内联的并没有错。因此,它们通常是导致代码膨胀的原因。应当编写通用函数,尤其是在库中,以尽量减少不需要的内联。举一个 wat 的例子:

1
2
3
4
5
6
7
8
9
10
11
// 公有泛型函数
// 处理不当会导致代码膨胀!
pub fn parse_str(wat: impl AsRef<str>) -> Result<Vec<u8>> {
  // 立即委托给非泛型函数。
  _parse_str(wat.as_ref())
}

// 分离式编译的友好的私有实现。
fn _parse_str(wat: &str) -> Result<Vec<u8>> {
    ...
}

参考列表

  1. Language Reference
  2. Rust performance book
  3. @alexcrichton 解释 inline。请注意,实际上编译时间成本比我描述的要糟糕——内联函数是基于单个 codegen-unit 而不是单个 crate 编译的。
  4. 更多 @alexcrichton
  5. 仍是 @alexcrichton

r/rust 中讨论。

现在有一个后续的帖子:并不总是 iCache

这个帖子是 One Hundred Thousand Lines of Rust 系列的一部分。

[译] 安全引导与镜像验证技术概览

为使用 Rouge 的 Jekyll 主题添加高亮指定行的功能