Inlining
调用和退出热函数、未内联的函数往往占执行时间的一小部分。内联这些函数可以提供小而简单的速度优势。
有四个内联属性可以用于Rust函数。
- None. 编译器会自己决定是否应该内联函数。这将取决于优化级别、函数的大小等。如果你没有使用链接时间优化,函数永远不会跨箱子内联。
#[inline]
. 这表明该函数应该内嵌,包括跨越crate边界。#[inline(always)]
. 这强烈建议该函数应该内嵌,包括跨越crate边界。#[inline(never)]
. 这强烈表示该函数不应被内联。
内联属性并不保证函数是否会被内联,但实际上 #[inline(always)]
会导致内联,除非在极端情况下。
内联不具有传递性。如果函数 f
调用函数 g
,并且您希望这两个函数在调用 f
的地方一起内联,那么这两个函数都应该标记为内联属性。
Simple Cases
最适合内联的是(a)非常小的函数,或者(b)只有一个调用点的函数。编译器通常会自己内联这些函数,即使没有内联属性。但是编译器不可能总是做出最好的选择,所以有时需要属性。 Example 1, Example 2, Example 3, Example 4, Example 5.
Cachegrind是一个很好的判断函数是否被内联的剖析器。当查看Cachegrind的输出时,如果(也只有当)函数的第一行和最后一行没有*标记事件数,你就可以判断该函数被内联了。 例如
. #[inline(always)]
. fn inlined(x: u32, y: u32) -> u32 {
700,000 eprintln!("inlined: {} + {}", x, y);
200,000 x + y
. }
.
. #[inline(never)]
400,000 fn not_inlined(x: u32, y: u32) -> u32 {
700,000 eprintln!("not_inlined: {} + {}", x, y);
200,000 x + y
200,000 }
添加内联属性后你应该再测一次,因为效果可能是不可预知的。有时它没有效果,因为附近一个之前内联的函数不再内联了。有时会拖慢代码的速度。内联也会影响编译时间,特别是交叉速率内联,它涉及到重复函数的内部表示。
Harder Cases
有时候,你有一个函数很大,有多个调用站点,但只有一个调用点是热调用点。你希望内联热调用点以提高速度,但不内联冷调用点以避免不必要的代码膨胀。处理的方法是将函数分成总是内联和从不内联的部分,后者调用前者。
例如,这个函数。
#![allow(unused)] fn main() { fn one() {}; fn two() {}; fn three() {}; fn my_function() { one(); two(); three(); } }
应该修改为如下函数
#![allow(unused)] fn main() { fn one() {}; fn two() {}; fn three() {}; // Use this at the hot call site. #[inline(always)] fn inlined_my_function() { one(); two(); three(); } // Use this at the cold call sites. #[inline(never)] fn uninlined_my_function() { inlined_my_function(); } }