inline 对于函数内联优化有效吗?

现代 C++ 中 inline 对于指示函数内联优化,有效吗?

It depends!

简短地说,这取决于编译器。

对于 GCC,一定程度上是有效的;而对于 Clang,基本上是无效的。

正文

很多 OIer 喜欢写 inline,但是也有人从来不写(反正我还在打 OI 的时候就没写过)

反对写 inline 的人的观点如下:

  • 在 C++11 及以后的标准中,inline 的含义是「允许重复定义」而不是「指示编译器进行内联优化」,所以写 inline 完全无效
  • 递归函数没法内联(因为递归的层数是在运行时确定的)
  • 编译器比你更清楚哪些函数应当被内联而哪些不应当
  • 习惯写 inline 可能会导致写出 inline func(...) 这类 Dev-C++ 可以编译过但是在 Linux 上编译不过的东西

我最初是赞成这些观点的。但是后来实测发现 GCC 加 inline 后生成的汇编代码明显比没有 inline 的长(甚至是递归函数!),我开始怀疑这个观点。但是遗憾的是,我不懂汇编,所以没法确定

最近在看现代 C++ 的书,忽然又想起了这个问题,于是编写了测试代码,开始实验~

虽然说,我依然不懂汇编,但是,现在有 LLM(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// test.cpp
#ifdef INLINE_ENABLE_TAG
#define INLINE_TAG inline
#else
#define INLINE_TAG
#endif

// 非常简单的函数
INLINE_TAG int small_func(int x)
{
constexpr double pi = 3.14;
double v = 4.0 / 3 * pi * x * x * x;
int res = v * 100;
return res;
}

// 含有循环的略微复杂的函数
INLINE_TAG int loop_func(int x)
{
if (x <= 3)
return -1;
int sum = x + 1;
int mid = -1;
int cnt = 2;
for (int i = 2; i * i < x; ++i)
if (x % i == 0)
{
sum += i + x / i;
cnt += 2;
mid = i;
}
if (cnt == 2 && mid == -1)
return -1;
int res = sum % 10;
res = res * 10 + mid % 10;
res = res * 10 + cnt % 10;
return res;
}

// 递归函数
INLINE_TAG int recursive_func(int x)
{
if (x < 0)
return 0;
constexpr int v[3] = {0, 2, 3};
if (x <= 2)
return v[x];
int s = x & 0b0101010101010101;
int res = recursive_func(x - 1) + recursive_func(x - 2) + s;
return res;
}

int main(int argc, char* argv[])
{
// 从命令行参数输入,避免使用 stdio 而产生大量无关汇编代码
int x = small_func(argc);
int y = loop_func(argc + 123);
int z = recursive_func(argc * 10);
int res = (x % 10) * 100 + (y % 10) * 10 + z % 10;
return res;
// 将结果通过 main 函数返回值返回,否则编译器会把所有代码都优化掉
}

编译命令:

1
2
3
4
g++ test.cpp -S -O2 -std=c++17 -o no_inline_gcc.s
g++ test.cpp -S -O2 -std=c++17 -DINLINE_ENABLE_TAG -o with_inline_gcc.s
clang++ test.cpp -S -O2 -std=c++17 -o no_inline_clang.s
clang++ test.cpp -S -O2 -std=c++17 -DINLINE_ENABLE_TAG -o with_inline_clang.s

我目前用的编译器都是比较新的版本,所以我选择了较新且较为稳定的 C++17 标准,并且打开了 O2

汇编输出:

先看一下生成代码的行数

1
2
3
4
5
6
7
8
$ wc no_inline_gcc.s
227 561 4025 no_inline_gcc.s
$ wc with_inline_gcc.s
343 855 6043 with_inline_gcc.s
$ wc no_inline_clang.s
345 1009 7959 no_inline_clang.s
$ wc with_inline_clang.s
231 672 5383 with_inline_clang.s

很奇怪,对于 clang,开 inline 代码居然变短了

粗略地看了一下汇编,似乎没有 inline 的情况下两个编译器对三个函数都有定义;clang 输出的代码中 main 函数都很大(而且开不开 inline 似乎没有什么区别),而 GCC 在开 inlinemain 函数体积增大的同时 recursive_func 的体积也增长了许多

我使用 diff 进行了比较,发现 clang 是否开 inline 生成的代码几乎没有区别,而减少的行数是前两个函数的定义。在没有 inline 的情况下,这些定义的函数似乎也没有被使用,应该是为后续可能的链接进行了预留

最后,我找 LLM 分析了这些汇编代码,结果是这样的(✅ = 内联,❎ = 不内联)

函数 GCC,无 inline GCC,inline Clang,无 inline Clang,inline
small_func
loop_func
recursive_func ✅ 递归展开

这恰好印证了我在汇编代码中的发现

此外,「递归函数没法内联」这类说法是错的

结论

这样,结论就已经很明确了:

  • 对于小函数,不论是 GCC 还是 Clang,编译器都会主动内联
  • 对于 Clang,指定 inline 在函数内联优化上完全没有作用,编译器会自行决定是否优化
  • 对于 GCC,指定 inline 对于函数内联优化有明显作用,甚至会让编译器进行递归展开
  • 对于所有编译器,inline C++ 标准指定的「运行重复定义」的语义都会保证

总体上来看,对于 Clang,inline 的语义与现代 C++ 标准规定的一致;而对于 GCC,inline 的语义更像是在传统 C++ 中的语义,即指示内联优化

至于为什么会是这样,我猜测这与 GCC 和 Clang 的历史有关。Clang 诞生于 2007 年,它作为 C++ 编译器成熟时正是 C++11 的年代,所以其设计上就是一个完全的现代 C++ 编译器;而 GCC 诞生时连 C++98 都没有,考虑到它继承了相当多的 90 年代的代码,在表现上更像是传统的 C++ 编译器也是完全合理的

那么,究竟写不写 inline 呢?

可写可不写。

不过,严谨地说,对于 OIer(编译器只有 GCC),卡常的时候或许写上比较好?(不过函数调用的这一点开销恐怕对于整体是微乎其微吧,况且即便是内联了,也不一定真的会提升性能)

至于工程上,如果使用 GCC 的话,或许应当按照 C++ 之父 Bjarne Stroustrup 的建议,测试之后再决定?

反正我不写,因为懒


吐槽:千问分析汇编代码完全在胡说八道,相比之下 Gemini 好很多

为什么御三家编译器中缺 MSVC,因为我这里没有