内联

对于热点、未内联的函数的进入和退出往往占据了执行时间的相当大的一部分。将这些函数内联可以带来一些小但容易的速度提升。

在 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();
}
}

示例 1示例 2