内联
对于热点、未内联的函数的进入和退出往往占据了执行时间的相当大的一部分。将这些函数内联可以带来一些小但容易的速度提升。
在 Rust 函数中有四种可以使用的内联属性。
- None。编译器会自行决定是否应该将函数内联。这将取决于优化级别和函数的大小等因素。非泛型函数永远不会跨 crate 边界内联,除非使用了链接时优化;而泛型函数可能会。
#[inline]
。这表示该函数应该内联,包括跨 crate 边界。#[inline(always)]
。这强烈建议该函数应该内联,包括跨 crate 边界。#[inline(never)]
。这强烈建议该函数不应该内联。
内联属性并不保证函数是否内联或不内联,但实际上 #[inline(always)]
会导致内联,除非在极端情况下。
内联不是传递性的。如果函数 f
调用函数 g
,并且您希望这两个函数在调用 f
的调用点一起内联,那么这两个函数都应该标记为内联属性。
简单情况
最适合内联的候选函数是:(a) 很小的函数,或者 (b) 只有一个调用点的函数。即使没有内联属性,编译器通常也会自动内联这些函数。但编译器并不能总是做出最佳选择,因此有时需要属性。 示例 1, 示例 2, 示例 3, 示例 4, 示例 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 }
添加内联属性后,您应该再次进行测量,因为效果可能是不可预测的。有时不会产生影响,因为以前内联的附近函数不再内联。有时会使代码变慢。内联还可能会影响编译时间,特别是涉及跨 crate 内联的情况,这会涉及到函数的内部表示的重复。
更困难的情况
有时您有一个大型函数,并且有多个调用点,但只有一个调用点是热点。您希望为了速度内联热点调用点,但不希望内联冷调用点以避免不必要的代码膨胀。处理这种情况的方法是将函数分割为始终内联和从不内联的变体,后者调用前者。
例如,这个函数:
#![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() {}; // 在热点调用点使用这个。 #[inline(always)] fn inlined_my_function() { one(); two(); three(); } // 在冷调用点使用这个。 #[inline(never)] fn uninlined_my_function() { inlined_my_function(); } }