inline 内联优化算是比较常见的编译器优化,这里简单介绍一下。

1、function call

为了进行函数调用,程序需要将调用参数放到栈或寄存器中,同时还需要保存一些寄存器到栈上,以免 callee 会覆盖到。函数执行的切换,对于代码局部性、寄存器使用、运行性能都会有不少的影响。

2、inline

inline 是通过把函数的实现体内置到调用点处,避免了函数调用的性能开销,虽然同时可能因为重复指令而带来代码体积的增大。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// inline 前
void A() {
	printf("A");
}

int B() {
	A();
}

// inline 后
void A() {
	printf("A");
}

int B() {
	printf("A");
}

(inline 后体积增大并不是必然,比如有研究 Inlining for Code Size Reduction,通过 inline 消除那些只会被调用一次的函数(需要有全局视角,通常结合 LTO 使用),反而减少了栈相关指令而减少体积。)

编译器并不能保证一定会进行 inline 优化,以下是其中的一些策略:

  • 在不同的编译单元中,通常不会 inline,因为可能找不到其实现和定义。除非开启了 LTO(link time optimization、链接时优化,之前文章介绍过)、模块化编译等功能。
  • C++ 类方法定义在类声明中,会被 inline,除非太大。
  • 静态(static) 函数大概率会被 inline。
  • 标记为 virtual 的 C++ 方法,不能被 inline。
  • 通过函数指针来调用的函数,不能被 inline。
  • 通过 lambda 表达式作为函数,可能会被 inline。
  • 太长的函数体,通常不会被 inline。有 inline-threshold 的相关参数可以控制阈值。
  • 使用 inline 关键字通常还是会被编译器所忽视,还有 __attribute__((always_inline))__attribute__((flatten)),这些如果不能 inline 会报编译警告。

实际情况会比较复杂,这里有篇有趣的文章列举了一些是否 inline 的案例 Does it inline? 可以看看。

3、inline 附赠的其他优化

inline 除了自己带来了性能上的优化,实际上,在 inline 之后的代码,也让其他优化看到了机会。所以在测试 inline 效果时,不能仅仅只考虑是 inline 进行的优化,还有其他优化也被影响到了。

下面将会简单地列出几个案例。

3.1、loop merging

比如在 inline 之前,有代码示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int find_min(const int* array, const int len) {
    int min = a[0]; 
    for (int i = 1; i < len; i++) {
        if (a[i] < min) { min = a[i]; }
    }
    return min;
}

int find_max(const int* array, const int len) {
    int max = a[0]; 
    for (int i = 1; i < len; i++) {
        if (a[i] > max) { max = a[i]; }
    }
    return min;
}

void main() {
    int* array, len, min, max;
    initialize_array(array, &len);
    min = find_min(array, len);
    max = find_max(array, len);
    ...
}

在进行 inline 优化的时候,find_minfind_max 函数都可能会被编译器进行 inline。因为 find_minfind_max 代码逻辑高度相似(以相同的范围来进行遍历,同时也没有数据依赖),所以当它们一起出现在一个地方时,就可以进行 loop merging(循环合并) 的优化。效果大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void main() {
    int* array, len, min, max;
    initialize_array(array, &len);
    min = a[0]; max = a[0];  // compiler inlined find_min and find_max
    for (int i = 0; i < len; i++) {  // compiler merged two loops int one
        if (a[i] < min) { min = a[i]; }
        if (a[i] > max) { max = a[i]; }
    }
    ...
}

这有效地减轻了 CPU 的任务,而且还提高了数据缓存的命中使用率。

注:需要编译器能够同时看到这两个函数的实现体,因为某些函数为外部引用,编译器看不到其实现,只能临时占位(待链接器补充),也就无从优化。本文不考虑这些情况。(关于跨模块优化可以看下我之前写的 LTO 优化的文章)

3.2、常量传播

在上图中,mul 函数直接被 inline 成 a * b,由于 a 和 b 的值,并没有在过程中被修改,实际上就可以被直接优化成 4 * 2,最后直接替换为 8。减少计算乘法的指令,优化了性能。

参考