~光~~头像
关注

【C/C++学习】inline

【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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--