【C/C++学习】inline
最近在学内核相关的书籍,其中一个关键字,对于它的理解不够深入,只知道它可以用于消除函数调用和返回带来的开销,但是发现它的存在不止于此
文章目录
整个总结
1️⃣ inline函数可以减少函数调用和返回开销
inline 不只是“把代码复制过去”,更重要的是它让调用者和被调用者可以作为一个整体参与优化。
2️⃣是否内联由编译器结合函数大小、调用频率和优化策略决定
3️⃣如果一个较大的函数被大量调用,全部展开会导致代码体积膨胀,增加指令缓存压力,反而可能降低性能
4️⃣内联函数通常放在头文件中,这样调用它的源文件在编译时可以看到完整的函数定义
因此 Linux 内核通常把短小、调用频繁、对性能比较敏感的函数定义为 static inline。static 用于限制函数的作用域,inline 用于表达内联意图。相比复杂宏,inline 函数具有更好的类型安全性和可读性,因此对于能够用函数表达的逻辑,内核通常更倾向于使用 inline 函数,而不是复杂的宏。
inline是什么
例如普通函数:
int add(int a, int b)
{
return a + b;
}
int x = add(1, 2);
普通情况下,程序执行到:
add(1, 2);
会发生一次函数调用:
调用者
↓
准备参数
↓
保存必要的寄存器/建立调用环境
↓
跳转到 add()
↓
执行 a+b
↓
返回
↓
恢复调用环境
↓
继续执行
如果这个函数特别短,比如:
static inline int add(int a, int b)
{
return a + b;
}
编译器有机会把调用:
int x = add(1, 2);
直接变成类似:
int x = 1 + 2;
也就是把函数的代码展开到调用位置附近。
这就是所谓:函数内联(Function Inlining),而对于非常短,调用非常频繁的函数,这种优化可能很有价值;
inline不一定保证函数会展开
inline 是给编译器的内联建议,并不保证函数一定会被展开。
现代 GCC/Clang 会结合:
- 函数大小
- 调用频率
- 优化级别
- 是否递归
- 编译器自己的代价模型
来决定是否真正 inline。
也就是根据编译优化,最后函数行为可能不同
例如:
gcc -O2
和:
gcc -O0
行为可能明显不同。
关于gcc -O
-O0 不做任何主动优化,变量全部存储在栈内存中,不进行函数内联,保留完整调试信息。
-O2 在 -O1 基础上额外开启:
函数内联:消除函数调用开销(压栈、跳转、返回)
循环优化:循环展开、强度削弱(乘法→加法)
死代码消除:移除永远不会执行的代码
常量传播与折叠:编译时直接算出常量表达式结果
指令调度:重排指令充分利用 CPU 流水线
| 维度 | -O0 (无优化) | -O2 (标准优化) |
|---|---|---|
| 优化程度 | 完全不优化,代码与源码逐行对应 | 开启死代码消除、常量折叠、函数内联、指令调度、循环优化等 |
| 执行速度 | 最慢 | 比 -O0 快 50%~100% |
| 代码体积 | 最大(变量全存栈,无压缩) | 较小 |
| 编译速度 | 最快 | 中等 |
| 调试体验 | 完美,断点精准对应源码 | 较差,变量可能被优化掉 |
| 用途 | 开发调试阶段 | 生产环境首选 |
为什么inline能带来进一步优化
让编译器获得更大的优化视野。
“让编译器获得更大的优化视野” = 原来编译器把函数调用看成一个“黑盒子”,内联之后,它能直接看到函数里面的具体代码,于是有更多机会把前后代码一起优化。
不内联时:函数像一个“黑盒子”
int add(int a, int b)
{
return a + b;
}
int foo()
{
int x = 10;
int y = 20;
return add(x, y);
}
如果 add() 没有被内联,编译器在 foo() 这里可以把它想象成:
foo()
│
│ 调用
↓
┌─────────────┐
│ add() │ ← 黑盒
│ │
│ return a+b │
└─────────────┘
编译器知道:
add(10, 20)
会返回一个值。
但是如果由于编译条件、编译单元、优化策略等原因,它没有把 add() 的实现纳入当前优化过程,那么调用点和函数内部就存在一个边界。
内联之后:黑盒子被打开了,如果:
static inline int add(int a, int b)
{
return a + b;
}
调用:
int foo()
{
int x = 10;
int y = 20;
return add(x, y);
}
内联以后,编译器可以把它看成:
int foo()
{
int x = 10;
int y = 20;
return x + y;
}
于是编译器现在可以同时看到:
foo()里的代码
+
add()里的代码
内联以后:
int foo()
{
return 10 + 20;
}
编译器继续优化:
return 30;
甚至最终可能变成非常简单的机器指令。这里发生了:
add(10,20)
↓
10 + 20
↓
30
为什么编译器能做到?因为它看到了 add() 内部:
return a + b;
如果函数是一个真正的黑盒:
foo()
↓
call add()
那么在调用点,它就不一定能直接利用 add() 内部的信息。
内联之后,编译器可以把调用者和函数内部代码放在一起分析,从而有机会进行:
- 常量传播
- 死代码消除
- 条件优化
- 寄存器分配优化
inline 不只是“把代码复制过去”,更重要的是它让调用者和被调用者可以作为一个整体参与优化。
但是,现在的 GCC/Clang 即使没有你写
inline,也可能自动内联。
为什么不能所有函数都inline
代价:代码膨胀
例如:
static inline int foo()
{
// 很多很多代码
}
假设 foo() 被调用了 1000 次。
如果真的全部展开:
foo代码
foo代码
foo代码
foo代码
...
重复1000次
那么程序体积就可能明显增加。
代码变大为什么会影响性能
这里涉及 Instruction Cache(指令缓存,I-Cache)。
CPU 执行程序的时候,并不是每条指令都直接从内存读取,而是会利用缓存:
CPU
↓
I-Cache
↓
内存
如果代码特别大:
代码体积变大
↓
I-Cache 能装下的有效代码比例下降
↓
Instruction Cache Miss 增多
↓
需要从更低层级缓存/内存取指令
↓
性能可能下降
所以 inline 并不是:
越多越快。
而是一个 trade-off:
inline
│
├── 优点
│ ├── 减少函数调用开销
│ └── 给编译器更多优化机会
│
└── 缺点
├── 代码体积变大
├── I-Cache 压力增大
└── 可能反而影响性能
为什么内核特别喜欢短小的 inline 函数?
因为内核里有大量:调用非常频繁、函数非常短、对性能比较敏感的操作。
例如链表、位操作、引用计数、一些简单的数据结构操作等。
假设:
static inline int foo(int x)
{
return x + 1;
}
如果它每秒调用几百万次甚至更多,那么函数调用本身的额外成本就可能值得关注。
所以内核中经常能看到:
static inline ...
不要一看到 inline 就认为:
“这个函数一定被展开了。”
而应该理解为:开发者认为这个函数适合内联,并通过 inline 给编译器提供这个意图。
为什么通常写 static inline
inline表示:这个函数适合进行内联。
static表示:这个函数的链接范围限制在当前编译单元。
编译器只有函数声明,并不知道函数具体实现。
所以 inline 函数经常直接写在 .h:
为什么内核更喜欢 inline 而不是复杂宏?
宏的问题是:宏本质上是预处理器进行的文本替换,没有真正的函数类型检查。
而 inline 函数:
static inline int max(int a, int b)
{
return a > b ? a : b;
}
属于真正的 C 函数,有:
- 参数类型检查
- 返回值类型
- 作用域
- 调试信息更清晰
但是 inline 不能完全替代宏,宏有一些 inline 做不到的事情。
例如条件编译:
#ifdef CONFIG_DEBUG
#define DEBUG_PRINT(...) ...
#endif
或者需要操作符、类型等预处理阶段能力的场景。
对于本来可以用函数表达的简单逻辑,内核通常优先考虑类型安全更好的 inline 函数,而不是复杂宏;但宏在条件编译、代码生成等场景仍然有作用。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_53131867/article/details/166645589



