avatar

Nihil

Nichts Hsu

  • 首页
  • 子域
  • 分类
  • 标签
  • 归档
  • 关于
首页 [译] Rust 中的内联
文章

[译] Rust 中的内联

发表于 2022/02/16 更新于 2022/12/30
作者 Aleksey Kladov 9 分钟阅读

免责声明

本文是对原博文《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 Allen 和 John 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]。特别注意实现 Deref 和 AsRef 类似的东西经常受益于内联。库无法预知所有的使用,不要过早地对未来的使用者抱有悲观态度才是正确的。请注意 #[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 系列的一部分。

翻译, Rust
rust 翻译
分享

最近更新

  • C++ Coroutine VS Rust Async
  • [C++] 深入了解左值与右值
  • [C++] 不使用标准库和 lambda 实现柯里化
  • [C++] std::function 是如何实现 lambda 递归的
  • GameMaker 8.0 从入门到入土
外部链接
  • 996.icu
  •  此博客的 Github 仓库
  •  Olimi 的个人博客

文章内容

相关文章

2022/02/17

[译] 并不总是 iCache

免责声明 本文是对原博文《It’s Not Always ICache》的无授权翻译转载,不享受任何著作权利,不用于任何商业目的,不以任何许可证进行授权,不对任何转载行为尤其是商业转载行为负责。一切权利均由原作者 Aleksey Kladov 保有。 本文中出现的所有第一人称均指代 Aleksey Kladov 而非译者本人。本文中对一些术语会额外附加英文原文注释,以帮助读者搜索相关概念...

2021/04/24

Rust 中函数与闭包与 Fn Traits 探讨

闭包 闭包,或者又名匿名函数,lambda 函数,它在官方文档中被定义为可以捕获环境的匿名函数。通常,闭包的定义具有以下的形式: let closure_name = |arg1: type1, arg2: type2| -&gt; return_type { // closure body } 在闭包定义中,可以省略参数的类型和返回值类型,Rust 将通过第一次调用该闭包时...

2021/06/07

Rust 中的闭包递归与 Y 组合子

λ 函数递归 λ 函数也即匿名函数,在 Rust 中体现为闭包(Closure)。在一些语言中,你可以简单地在 λ 函数内调用自己实现递归,例如在 JavaScript 中实现一个阶乘: fact = n =&gt; { if (n == 0) return 1; else return n * fact(n - 1); } console.log(fact(5)) //...

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

[译] 并不总是 iCache

© 2023 Nichts Hsu. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

热门标签

编程语言 教程 c++ rust android c++20 usb linux qt 翻译

发现新版本的内容。