在 C
语言中,inline
关键字是一种编译器提示,用于建议编译器将函数调用替换为函数体本身。这种优化技术可以减少函数调用的开销,特别是对于那些频繁调用但实现简单的函数来说更为有效。除了标准的 inline
关键字外,一些编译器还提供了额外的控制机制,如always_inline
和 noinline
,以提供更精细的控制。
我们接下来主要讲一下跟内联函数相关的两个属性:always_inline
和 noinline
。这两个属性的用途是告诉编译器:编译时,对我们指定的函数内联展开或不展开。
定义:当一个函数声明或定义前加上 inline
关键字时,它指示编译器尝试将该函数的调用展开而不是进行常规的函数调用。这可以减少程序运行时的性能损失。
标准:C99
及以后的标准。
用途:建议编译器将函数内联化。
使用场景:适用于短小且经常被调用的函数。
语法:
inline int func(int x, int y) {
return x + y;
}
定义:这是一个非标准的扩展,通常由特定的编译器(如 GCC
)支持。它强制编译器将指定的函数内联,即使这样做可能会导致代码膨胀。
标准:编译器特定扩展(GCC
等)。用途:强制编译器将函数内联化,即使会导致代码膨胀。
使用场景:当开发者确定某个函数必须被内联以达到最佳性能时使用。
语法(GCC
):
inline __attribute__((always_inline)) int func(int x, int y) {
return x * y;
}
定义:同样是一个编译器特定的扩展,它告诉编译器不要将标记的函数内联,即使它们被声明为inline。
标准:编译器特定扩展(GCC
等)。用途:禁止编译器将函数内联化,即使函数被声明为 inline
。
使用场景:当需要确保某些函数不会被内联,例如为了调试目的或者避免代码体积过大时。
语法(GCC
):
__attribute__((noinline)) int func(int x, int y) {
return x - y;
}
【拓展:关键字归属说明】
inline:这是
C
语言标准的一部分,从C99
开始引入。inline
关键字用于建议编译器将函数调用内联化,以减少函数调用的开销。always_inline 和 noinline:这两个关键字是编译器特定的扩展,主要由
GCC
(GNU Compiler Collection
)和其他一些编译器支持。它们提供了更细粒度的控制,超越了标准C
语言的inline
关键字。
内联函数使用 inline
关键字声明即可,有时还会结合 static
和 extern
修饰符一起使用。使用 inline
声明一个内联函数,类似于使用 register
关键字声明一个变量。这两种关键字都是向编译器提出建议,而不是强制命令。使用 register
修饰变量时,编译器被建议将该变量存储在寄存器中,以提高程序的运行效率。然而,编译器是否会遵循这一建议,取决于寄存器资源的可用性和变量的使用频率。
【思考:内联函数为什么常使用 static 修饰?】
在
Linux
内核中,大量的内联函数定义在头文件中,并且常常使用static
修饰。关于这一点,网上有很多讨论,但核心原因可以从C
语言和C++
的角度来理解。Linux
内核作者Linus Torvalds
也对此有过解释:“static inline” 意味着“如果我们需要这个函数,但不内联它,那么就在这个编译单元中生成一个静态版本。”而 “extern inline” 则意味着“我实际上有一个外部定义的函数,但如果需要内联它,这里提供了一个内联版本。”
我的理解如下:
为什么内联函数要定义在头文件中?内联函数通常定义在头文件中,因为它们可以像宏一样使用。任何需要使用这些内联函数的源文件,只需包含相应的头文件,即可直接使用这些函数,而不需要重复定义。这样可以简化代码管理和维护。
为什么内联函数要用 static 修饰?尽管我们使用
inline
关键字定义了内联函数,但编译器并不一定会将其内联展开。如果多个源文件都包含了同一个内联函数的定义,编译时可能会出现重定义错误。通过使用static
修饰,可以将函数的作用域限制在各自的编译单元内,从而避免重定义错误。
同样,当一个函数使用 inline
关键字修饰时,编译器在编译时并不一定会将其内联展开。编译器会根据多种因素来决定是否内联展开,这些因素包括函数体的大小、函数体内是否存在循环结构、是否有指针操作、是否有递归调用以及函数的调用频率等。GCC
编译器通常不会在默认情况下对内联函数进行展开,默认的 GCC
编译的优化选项是 -O0
的,这样是不会内联的,有些版本甚至无法编译通过,只有当编译优化级别设置为 -O2
或更高时,编译器才会考虑是否进行内联展开。
当我们使用 noinline
和 always_inline
属性对一个内联函数进行声明后,编译器的行为就变得确定了。使用 noinline
声明,明确告知编译器不要内联展开该函数;而使用 always_inline
属性声明,则明确告知编译器必须内联展开该函数。
通过这种方式,开发者可以更精细地控制函数的内联行为,从而优化程序的性能。
【拓展:inline、always_inline、noinline的区别】
inline:仅仅是建议编译器内联,但不一定内联。
always_inline :强制内联。
noinline:强制不内联。
我们可以编写一个 C
语言程序,然后使用反汇编工具(如 objdump
)来查看不同内联策略下的汇编代码。这将帮助我们直观地理解 inline
、always_inline
和 noinline
的实际效果。首先,编写一个 C
语言程序,包含三种不同内联策略的函数。
头文件 inline_functions.h
:
#ifndef INLINE_FUNCTIONS_H
#define INLINE_FUNCTIONS_H
// 使用inline
inline int addInline(int x, int y) {
return x + y;
}
// 使用always_inline
__attribute__((always_inline)) inline int addAlwaysInline(int x, int y) {
return x * y;
}
// 使用noinline
__attribute__((noinline)) int addNoInline(int x, int y) {
return x - y;
}
#endif // INLINE_FUNCTIONS_H
源文件 inline_test.c
:
#include
#include
#include "inline_functions.h"
int main() {
int result;
// 调用addInline
result = addInline(10, 20);
printf("addInline(10, 20) = %d\n", result);
// 调用addAlwaysInline
result = addAlwaysInline(10, 20);
printf("addAlwaysInline(10, 20) = %d\n", result);
// 调用addNoInline
result = addNoInline(10, 20);
printf("addNoInline(10, 20) = %d\n", result);
return 0;
}
让我们使用 -O2
优化级别编译程序,以确保编译器能够应用内联优化。
gcc -O2 -g -o inline_test inline_test.c
使用 objdump
工具反汇编生成的目标文件。
objdump -d -S inline_test > inline_test.asm
打开生成的 inline_test.asm
文件,找到 main
函数的反汇编代码,观察不同内联策略的效果。
main
函数的反汇编代码如下:
int main() {
4004b0: 48 83 ec 08 sub $0x8,%rsp
int result;
// 调用addInline
result = addInline(10, 20);
printf("addInline(10, 20) = %d\n", result);
4004b4: be 1e 00 00 00 mov $0x1e,%esi
4004b9: bf 98 06 40 00 mov $0x400698,%edi
4004be: 31 c0 xor %eax,%eax
4004c0: e8 db ff ff ff callq 4004a0
// 调用addAlwaysInline
result = addAlwaysInline(10, 20);
printf("addAlwaysInline(10, 20) = %d\n", result);
4004c5: be c8 00 00 00 mov $0xc8,%esi
4004ca: bf b0 06 40 00 mov $0x4006b0,%edi
4004cf: 31 c0 xor %eax,%eax
4004d1: e8 ca ff ff ff callq 4004a0
// 调用addNoInline
result = addNoInline(10, 20);
4004d6: be 14 00 00 00 mov $0x14,%esi
4004db: bf 0a 00 00 00 mov $0xa,%edi
4004e0: e8 0b 01 00 00 callq 4005f0
printf("addNoInline(10, 20) = %d\n", result);
4004e5: bf ce 06 40 00 mov $0x4006ce,%edi
4004ea: 89 c6 mov %eax,%esi
4004ec: 31 c0 xor %eax,%eax
4004ee: e8 ad ff ff ff callq 4004a0
return 0;
}
4004f3: 31 c0 xor %eax,%eax
4004f5: 48 83 c4 08 add $0x8,%rsp
4004f9: c3 retq
汇编代码解析如下:
4004b0: 48 83 ec 08 sub $0x8,%rsp
4004b4: be 1e 00 00 00 mov $0x1e,%esi
4004b9: bf 98 06 40 00 mov $0x400698,%edi
4004be: 31 c0 xor %eax,%eax
4004c0: e8 db ff ff ff callq 4004a0
mov $0x1e,%esi:将 30
(即 10 + 20
的结果)加载到 %esi
寄存器,作为 printf
的第一个参数。
mov $0x400698,%edi:将格式字符串的地址 0x400698
加载到 %edi
寄存器,作为 printf
的第二个参数。
xor %eax,%eax:清零 %eax
寄存器,表示没有浮点数参数。
callq 4004a0 printf@plt:调用 printf
函数。
4004c5: be c8 00 00 00 mov $0xc8,%esi
4004ca: bf b0 06 40 00 mov $0x4006b0,%edi
4004cf: 31 c0 xor %eax,%eax
4004d1: e8 ca ff ff ff callq 4004a0
mov $0xc8,%esi:将 200
(即 10 * 20
的结果)加载到 %esi
寄存器,作为 printf
的第一个参数。
mov $0x4006b0,%edi:将格式字符串的地址 0x4006b0
加载到 %edi
寄存器,作为 printf
的第二个参数。
xor %eax,%eax:清零 %eax
寄存器,表示没有浮点数参数。
callq 4004a0 printf@plt:调用 printf
函数。
4004d6: be 14 00 00 00 mov $0x14,%esi
4004db: bf 0a 00 00 00 mov $0xa,%edi
4004e0: e8 0b 01 00 00 callq 4005f0
mov $0x14,%esi:将 10
加载到 %esi
寄存器,作为 addNoInline
的第一个参数。
mov $0xa,%edi:将 20
加载到 %edi
寄存器,作为 addNoInline
的第二个参数。
callq 4005f0 addNoInline
函数。
4004e5: bf ce 06 40 00 mov $0x4006ce,%edi
4004ea: 89 c6 mov %eax,%esi
4004ec: 31 c0 xor %eax,%eax
4004ee: e8 ad ff ff ff callq 4004a0
mov $0x4006ce,%edi:将格式字符串的地址 0x4006ce
加载到 %edi
寄存器,作为 printf
的第二个参数。
mov %eax,%esi:将 addNoInline
的返回值(存储在 %eax
寄存器中)加载到 %esi
寄存器,作为 printf
的第一个参数。
xor %eax,%eax:清零 %eax
寄存器,表示没有浮点数参数。
callq 4004a0 printf@plt:调用 printf
函数。
4004f3: 31 c0 xor %eax,%eax
4004f5: 48 83 c4 08 add $0x8,%rsp
4004f9: c3 retq
xor %eax,%eax:清零 %eax
寄存器,表示 main
函数的返回值为 0
。
add $0x8,%rsp:恢复栈指针。
retq:返回到调用者。
上述解析总结如下:
addInline:编译器将 addInline
内联展开了,因此在 main
函数中直接计算了 10 + 20
的结果,并将结果 30
直接传递给 printf
。
addAlwaysInline:编译器将 addAlwaysInline
内联展开了,因此在 main
函数中直接计算了 10 * 20
的结果,并将结果 200
直接传递给 printf
。
addNoInline:编译器没有将 addNoInline
内联展开,因此在 main
函数中通过调用
addNoInline
函数来计算 10 - 20
的结果,然后将结果传递给 printf
。
通过反汇编代码,我们可以清楚地看到不同内联策略对函数调用的影响。
使用 inline
、always_inline
和 noinline
关键字可以帮助程序员更精确地控制函数内联行为,从而影响程序的性能和可维护性。虽然 inline
是 C
标准的一部分,但always_inline
和 noinline
则是编译器提供的扩展功能,使用时应确保目标编译器支持这些特性。合理利用这些工具可以优化关键路径上的代码执行效率,但也需要注意过度使用内联可能导致代码膨胀的问题。在实际开发中,应该根据具体需求和测试结果来决定是否使用以及如何使用这些内联选项。