1.什么是内联函数:以空间换时间
编译器在编译阶段,会对内联函数进行语法分析和类型检查。如果确认安全且有性能收益,编译器会将函数体直接嵌入到调用处,就像把代码拷过去一样,但带有完整的类型安全机制。
2.内联函数如何减少调用开销
(1)我们先来看普通函数的调用,这涉及到栈帧操作:
保存现场:把当前的寄存器值、返回地址压入栈。
参数传递:把参数压入栈或放入寄存器。
跳转:指令跳转到函数代码地址。
执行:运行函数体。
恢复现场:弹出栈,恢复寄存器,跳转回原来的位置。
(2)而内联函数,如果编译器决定执行内联(编译器也有可能不执行内联,即使你声明了,他会有一个开销判断),就会直接省略前几步,将第 4 步函数体本身插入在主流程中。
3.编译器具体是怎样优化内联函数的呢?
(1)替换
将目标函数体代码直接替换主函数的调用节点,并且用实参替换形参。这样一来,“函数调用”这个概念消失了,取而代之的是平铺直叙的指令序列。
(2)连锁优化:暴露上下文信息,让编译器能进行常量折叠、死代码消除等更高级的优化。
以下面这个代码为例:
// 简单的内联函数 inline int square(int x) { if (x < 0) return 0; // 假设负数返回0 return x * x; } void businessLogic() { int val = 10; int result = square(val); // 调用处 }(1)代码展开: 编译器先把代码变成这样:
int val = 10; int result; if (val < 0) result = 0; else result = val * val;(2)常量传播 (Constant Propagation): 编译器发现val是10,是已知的常量。它会把10代入后续计算:
if (10 < 0) ... // 编译器发现 10 < 0 永远为 false else result = 10 * 10;(3)死代码消除 (Dead Code Elimination): 既然10 < 0永远为假,那个if分支就是“死代码”,直接砍掉。
result = 10 * 10;(4)常量折叠 (Constant Folding):10 * 10可以在编译期算出结果100。
int result = 100;4.并不是所有函数都可以内联
函数体过大:如果函数有 100 行代码,内联 10 次,代码体积就膨胀 1000 行。这会导致最终的可执行文件变大,更糟糕的是会撑爆 CPU 的指令缓存,导致 Cache Miss 率飙升,反而变慢。
递归函数:如果是无限递归,编译器没法内联(无法展开无穷次)。当
虚函数:虚函数通常需要在运行时查表(虚函数表 )决定调用哪个,编译期不知道调用谁,所以很难内联。