inline 的作用是建议编译器在调用点把函数体直接展开,省掉函数调用开销。它发生在编译期,运行期没有额外逻辑。
函数调用的开销
1 | int sum(int x, int y) |
调用一次 sum函数的完整流程包括:
- 从右向左压实参
call压入返回地址- 进入函数后保存调用方
ebp、开辟栈帧 - 执行函数体
- 回退栈帧、恢复
ebp、执行ret - 回到调用方后清理参数
完整指令序列见《从汇编指令角度分析函数调用堆栈的详细过程》。
函数体越简单、调用越频繁,调用开销的占比就越高。假如在循环里调用 sum 一千万次,大部分时间可能都花在压栈、建栈帧和退栈帧上,而不是 x + y 本身。所以这种比较简单的函数,适合使用inline
inline 在编译期展开
给函数加上 inline:
1 | inline int sum(int x, int y) |
编译器看到 sum(a, b),直接把函数体放到调用点,相当于:
1 | int ret = a + b; |
调用点的汇编是:(MSVC ,Release(/O2)。符号名做了简化)
1 | mov ecx, DWORD PTR [b] ; 取 b |
对比不带inline的调用点:
1 | mov eax, DWORD PTR [b] |
inline下少了压栈、函数栈帧的创建、回退。不带inline的函数的调用细节见《C++ 形参带默认值的函数》。
inline 只是建议
inline 不是命令,编译器可以拒绝展开。常见的不展开情况:
| 情况 | 原因 |
|---|---|
| 递归函数 | 编译期不知道要递归多少层,没法确定展开几次 |
| 函数体太大 | 展开后代码膨胀,收益抵不过体积 |
递归函数:展开的前提是编译期能确定函数体的全部内容,而递归的层数要等运行期看递归条件。编译器在编译阶段不执行代码,自然不知道要展开多少层。
另外,不写 inline 的小函数也可能被自动内联。现代编译器会自己判断成本收益,inline 更多是表达意图。最终是否展开,由编译器决定。
Debug 下不会内联
调试时需要在函数里打断点、看栈帧。如果函数体被展开,断点就没地方放,栈帧也没有了,调试器无法按函数单步。
怎么确认真的内联了
inline 只是建议,写不写和最终展不展开是两回事。要确认,只能看编译器生成的产物:反汇编,或者目标文件的符号表。
看反汇编:调用点还有没有 call
call 是函数调用的标志。只要调用点还有 call,函数就是被调用,而不是被展开;内联成功的调用点,会直接出现函数体的指令。
实际验证时,拿反汇编来看:
- MSVC:项目属性 → C/C++ → 输出文件 → 汇编输出,选带源代码的汇编(命令行对应
/FAs),编译后中间目录会生成.asm文件。要选 Release 配置,或者确认优化是开启的——Debug 默认不优化,看到的永远是普通的函数调用。 - GCC/Linux:把前面的代码存成
test.cpp,先g++ -O2 -c test.cpp生成目标文件,再objdump -d test.o。-O2是开启优化的级别;不写的话默认是-O0,不会内联。
反汇编看的是调用点,这是最直接的证据。
查符号表:函数有没有保留独立定义
符号表记录目标文件里的函数、全局变量等符号,链接器靠它把调用方和被调方对上(详见《从编译器角度理解 C++ 代码的编译和链接》)。
GCC 上可以用 objdump -t test.o | grep sum 查看。以前面的代码为例,三组对照的结果是:
- 给函数加
inline,用-O2优化编译:符号表里找不到sum。函数体已经并进调用方,不需要再留一份独立定义。 - 不加
inline,同样用-O2编译:sum的符号还在。非 inline 函数即使被编译器自动内联,也要保留一份定义,供别的编译单元链接。 - 加
inline,但用-O0(不优化)编译:sum的符号也还在。没有优化,函数不会被展开。
符号表只能说明函数有没有保留独立定义,不能单独证明调用点是否展开——第二种情况就是已经自动内联,符号却还在。
MSVC 上符号表不能作数
在 MSVC 上给函数加 inline,用 /O2 优化编译,目标文件里仍然保留 sum 的符号。原因是 inline 函数是外部链接,通常写在头文件里、被多个 .cpp 包含;每个 .cpp 都会生成一份定义,链接器通过 COMDAT(pick any)只挑一份,避免重复定义。所以符号保留是正常的,不能看到符号就认为没内联。
所以,如果想确认某个调用点有没有内联,看反汇编里还有没有 call是比较好的方式,因为这个判据在 MSVC 和 GCC 上都成立。而符号表在 GCC 上可以作为辅助验证,在 MSVC 上会因为 COMDAT 保留符号而误判。
内联函数和普通函数的区别
- 调用开销:普通函数调用要压实参、压返回地址、开辟和回退栈帧;内联函数在编译期把函数体展开到调用点,这些开销都没有。
- 适用场景:函数体简单、调用频繁时收益最大。函数体复杂时,调用开销占比小,展开还会让代码膨胀,通常不值得。
- 符号:GCC 上内联成功后不再为函数生成独立符号;MSVC 上仍会保留 COMDAT 符号。所以符号表只能辅助判断,不能跨编译器通用。
- 强制性:
inline只是建议。递归函数、大函数通常不会被展开;是否展开最终由编译器决定,并且要有优化——Debug 配置默认不优化,不会展开,Release 配置默认开启优化,才可能展开。
小结
inline是给编译器的建议:内联成功后,函数体在编译期展开到调用点,省掉压参、call、栈帧开辟和回退。- 递归函数和函数体过大的函数通常不会被内联,写了
inline也不保证。 - 内联依赖优化开关:Debug 默认不优化,不会展开;Release 默认优化,才可能展开。
- 验证时看反汇编的调用点还有没有
call,这个判据通用;符号表在 GCC 上可用,在 MSVC 上会因为 COMDAT 保留符号而误判。