C之(9)函数内联(inline)深入分析
Author: Once Day Date:2023年8月9日
漫漫长路,有人对你微笑过嘛…
参考引用文档:
文章目录
1. 概述
函数内联并非是一种编程技巧,而是一种优化技巧。对于编程层级代码而言,我们总是希望代码的抽象层级较高,尽可能复用逻辑相同的部分,从而降低代码量和维护难度。
但是高度抽象的代码,难免会有一些冗余判断,或是设计不当,或是强行融合,或是技术不够…,不管什么原因,总会存在一些制约条件使得抽象复用的代码在性能方面存在一些不足。
一种理想的场景是,如果在编程层级维护高度抽象的复用代码,在汇编层级生成特化的执行指令,便可以达到性能和维护性的平衡。
但是代价是什么?没错,代码体积会极大膨胀,所以,只能对于有性能要求的代码段进行优化,毕竟大多时候性能瓶颈都不在执行速度上面。
下面分析使用的编译工具是gcc(版本11, 不同版本表现各异),无需在意不同编译器的差别,本文意在追求思维上的分析方法,而不是在于能实现什么功能,若能举一反三,其他编译器不过也是换汤不换药罢了。
2. Gcc内联说明
2.1 何时进行内联
这里的何时有两个理解,一个是从编译的几大步骤来看,分为预处理、汇编、链接三步,前两步都是针对单个C源文件而言。一般而言内联函数的处理都是在汇编这一步,因此内联函数都是本地函数,外部函数是到链接这一步才确定。不能排除某些编译器版本支持链接时进行内联函数展开,本文暂认为内联只能在汇编阶段进行,这也是当前测试的gcc编译器表现。
GCC在默认优化级别(-O0)下不会进行函数内联,函数内联在较高的优化级别下才会启用:
-
-O1
:启用基本优化级别。GCC在此级别下会进行一些简单的优化,包括函数内联。但是,它只会内联那些被标记为inline
的函数。 -
-O2
:启用更高级别的优化。GCC在此级别下会进行更多的优化,包括函数内联。除了被标记为inline
的函数外,GCC还会根据自身的判断决定是否内联其他函数。 -
-O3
:启用最高级别的优化。GCC在此级别下会进行更加积极的优化,包括函数内联。它会尝试内联更多的函数,以提高性能。此级别会产生更高效的代码,但也可能导致编译时间增加。
除了以上三个主要的优化级别,GCC还提供了一些其他的优化选项,如-Os(优化代码大小)和-Og(优化调试体验)。这些选项也可以启用函数内联,但具体的内联行为取决于编译器的决策策略和代码结构。
一般有两种定义内联函数的方式:
static inline int add_one(int ori)
{
return ori + 1;
}
这种方式是最常见的定义方式,如果add_one不存在符号被非内联引用的情况,那么最终的二进制文件里面是没有add_one
这个符号的。第二种方式如下:
extern int add_one(int ori);
inline int add_one(int ori)
{
return ori + 1;
}
这种方式,对当前C源文件提供函数内联支持,同时保存符号和代码,用于链接阶段给外部的函数调用使用,相当于同时支持函数内联和函数调用。
在一般的情况下,只考虑第一个定义方式即可,因为本文的关注点是内联函数展开,没有展开的内联函数和其他函数无任何区别。
如果优化等级足够高,即使函数没有inline
修饰,那些很短的函数一样会主动内联展开。如果某些函数我们总是希望它们展开,可以使用如下的属性进行修饰,即使-O0
优化等级一样也会进行内联。
/* Prototype. */
inline void foo (const char) __attribute__((always_inline))
当一个函数既是内联的又是静态的,如果对函数的所有调用都集成到调用者中,并且函数的地址从未被使用,那么函数自己的汇编代码也从未被引用。在这种情况下,GCC实际上不会为函数输出汇编代码,除非指定了选项-fkeep-inline-functions
。如果存在非内联调用,则像往常一样将函数编译为汇编代码。如果程序引用它的地址,也必须像往常一样编译函数,因为它不能内联。
注意,函数定义中的某些用法可能使其不适合内联替换。这些用法包括:可变参数函数、alloca
的使用、使用动态goto、非局部goto的使用、嵌套函数的使用、setjmp的使用、__builtin_longjmp
的使用以及__builtin_return
或__builtin_apply_args
的使用。当标记为内联的函数不能被替换时,使用-Winline
则会发出警告,并给出失败的原因,所以编译时可以加上这个选项,此外还可以使用-fopt-info-inline
查看内联相关的优化信息。
2.2 内联函数分析
下面是一段简单的代码:
/* simple-inline.c */
#include <stdio.h>
static inline int add_one(int ori)
{
return ori + 1;
}
int main(void)
{
int a = 10;
a = add_one(a);
return a;
}
使用下面的命令来编译,并看看实际输出(直接输出汇编或者二进制再反汇编都可以):
# 二进制输出再反汇编
gcc -o simple-inline.out simple-inline.c -std=c99 -fopt-info-inline -Winline -O[n]
objdump -d simple-inline.out > simple-inline.s
# 直接输出汇编文件
gcc -S -o simple-inline.S simple-inline.c -std=c99 -fopt-info-inline -Winline -O[n]
其实一般直接查看汇编输出即可判断内联展开情况,但是为了保险起见,反汇编的代码更有说服力,-fopt-info-inline
也能输出内联函数展开的情况,但是在低版本的gcc不太可用。
(1) 无优化的等级下的内联情况:
ubuntu->gcc-test:$ gcc -o simple-inline.out simple-inline.c -std=c99 -fopt-info-inline -Winline -O0
ubuntu->gcc-test:$ objdump -d simple-inline.out > simple-inline.s
对应汇编如下:
0000000000001129 <add_one>:
1129: 55 push %rbp
112a: 48 89 e5 mov %rsp,%rbp
112d: 89 7d fc mov %edi,-0x4(%rbp)
1130: 8b 45 fc mov -0x4(%rbp),%eax
1133: 83 c0 01 add $0x1,%eax
1136: 5d pop %rbp
1137: c3 ret
0000000000001138 <main>:
1138: f3 0f 1e fa endbr64
113c: 55 push %rbp
113d: 48 89 e5 mov %rsp,%rbp
1140: 48 83 ec 10 sub $0x10,%rsp
1144: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
114b: 8b 45 fc mov -0x4(%rbp),%eax
114e: 89 c7 mov %eax,%edi
1150: e8 d4 ff ff ff call 1129 <add_one>
1155: 89 45 fc mov %eax,-0x4(%rbp)
1158: 8b 45 fc mov -0x4(%rbp),%eax
115b: c9 leave
115c: c3 ret
很显然,无优化等级下默认是不会进行展开的,这个优化等级一般用于debug调试,这样方面定位函数调用栈,所以还是很有用处。
(2) 强制指定函数内联,这里在对add_one
函数使用__attribute__((always_inline))
修饰
ubuntu->gcc-test:$ gcc -o simple-inline.out simple-inline.c -std=c99 -fopt-info-inline -Winline -O0
simple-inline.c:20:9: optimized: Inlining add_one/0 into main/1 (always_inline).
ubuntu->gcc-test:$ objdump -d simple-inline.out > simple-inline.s
编译信息里面已经提示成功内联了函数add_one
,再看看main函数汇编,如下:
0000000000001129 <main>:
1129: f3 0f 1e fa endbr64
112d: 55 push %rbp
112e: 48 89 e5 mov %rsp,%rbp
1131: c7 45 f8 0a 00 00 00 movl $0xa,-0x8(%rbp)
1138: 8b 45 f8 mov -0x8(%rbp),%eax
113b: 89 45 fc mov %eax,-0x4(%rbp)
113e: 8b 45 fc mov -0x4(%rbp),%eax
1141: 83 c0 01 add $0x1,%eax
1144: 89 45 f8 mov %eax,-0x8(%rbp)
1147: 8b 45 f8 mov -0x8(%rbp),%eax
114a: 5d pop %rbp
114b: c3 ret
可以看到,函数成功内联,并且代码长度还有减少,这也是函数内联的作用之一,裁剪无用代码和指令,因为优化等级不够高,因此效果不太明确。二进制文件里没有add_one
函数符号,因为没有非内联调用和对其的地址引用,所以编译器无需生成对应的函数代码。
(3) 高优化等级下的函数内联情况,
ubuntu->gcc-test:$ gcc -o simple-inline.out simple-inline.c -std=c99 -fopt-info-inline -Winline -O1
simple-inline.c:20:9: optimized: Inlining add_one/13 into main/14 (always_inline).
ubuntu->gcc-test:$ objdump -d simple-inline.out > simple-inline.s
同样也内联了,并且main函数的汇编指令大大缩减:
0000000000001129 <main>:
1129: f3 0f 1e fa endbr64
112d: b8 0b 00 00 00 mov $0xb,%eax
1132: c3 ret
实际代码里面的计算结果是一个固定的常数,所以优化之后直接输出最终的常数结果,这也是内联函数的预期优化效果之一,可以裁剪一些在编译期确定的无用情况。
2.3 内联实例分析
正常情况下,由于内联函数过大,可能会造成无法内联的情况,编译器会直接跳过,而没有错误,如下:
#include <stdio.h>
static inline int add_one(int ori) { return ori + 1;}
/* clang-format off */
#define VPP_REPEAT_1(op) op(1)
#define VPP_REPEAT_2(op) VPP_REPEAT_1(op); op(2)
#define VPP_REPEAT_3(op) VPP_REPEAT_2(op); op(3)
#define VPP_REPEAT_4(op) VPP_REPEAT_3(op); op(4)
#define VPP_REPEAT_5(op) VPP_REPEAT_4(op); op(5)
#define VPP_REPEAT_6(op) VPP_REPEAT_5(op); op(6)
#define VPP_REPEAT_7(op) VPP_REPEAT_6(op); op(7)
#define VPP_REPEAT_8(op) VPP_REPEAT_7(op); op(8)
#define VPP_REPEAT_9(op) VPP_REPEAT_8(op); op(9)
#define VPP_REPEAT_10(op) VPP_REPEAT_9(op); op(10)
#define VPP_REPEAT_11(op) VPP_REPEAT_10(op); op(11)
#define VPP_REPEAT_12(op) VPP_REPEAT_11(op); op(12)
#define VPP_REPEAT_13(op) VPP_REPEAT_12(op); op(13)
#define VPP_REPEAT_14(op) VPP_REPEAT_13(op); op(14)
#define VPP_REPEAT_15(op) VPP_REPEAT_14(op); op(15)
#define VPP_REPEAT_16(op) VPP_REPEAT_15(op); op(16)
#define VPP_REPEAT(op, times) VPP_REPEAT_##times(op)
/* clang-format on */
static inline int add_many(int a, int num)
{
#define DO_ONE(i) \
{ \
if (i > num) { \
break; \
} \
a = add_one(a); \
printf("[%i]add one -> %d .\n", i, a); \
}
do {
VPP_REPEAT(DO_ONE, 16);
} while (0);
printf("Add many over.\n");
return a;
}
static inline int add_much(int a)
{
a = add_many(a, a);
a = add_many(a, a);
return a;
}
int main(void)
{
int a = 10;
a = add_many(a, 16);
return add_much(a);
}
这段代码,利用宏来重复函数,在没有优化等级的情况,默认是通过宏展开,插入16个add_one
函数到add_many
函数里面,然后逐个调用。开启优化编译之后,add_one
会全部内联,并且add_many
也会内联到main函数里面,最终的二进制文件是没有add_one
和add_many
符号的。如下:
ubuntu->gcc-test:$ gcc -o simple-inline.out simple-inline.c -std=c99 -fopt-info-inline -Winline -O1
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:60:13: optimized: Inlined add_many/14 into main/15 which now has time 23.273261 and size 26, net change of -115.
simple-inline.c: In function ‘main’:
simple-inline.c:38:19: warning: inlining failed in call to ‘add_many’: call is unlikely and code size would grow [-Winline]
38 | static inline int add_many(int a, int num)
| ^~~~~~~~
simple-inline.c:67:13: note: called from here
67 | a = add_many(a, 16);
| ^~~~~~~~~~~~~~~
simple-inline.c:38:19: warning: inlining failed in call to ‘add_many’: --param max-inline-insns-single limit reached [-Winline]
38 | static inline int add_many(int a, int num)
| ^~~~~~~~
simple-inline.c:60:9: note: called from here
60 | a = add_many(a, a);
| ^~~~~~~~~~~~~~
simple-inline.c:38:19: warning: inlining failed in call to ‘add_many’: --param max-inline-insns-single limit reached [-Winline]
38 | static inline int add_many(int a, int num)
| ^~~~~~~~
simple-inline.c:59:9: note: called from here
59 | a = add_many(a, a);
上面的编译信息输出中,警告信息提示函数内联失败,因为函数内联后代码体积增加过大,超过了限制,从而导致内联失败。遇到这种情况可以通过__attribute__((always_inline))
来强制内联,一般会定义一个宏,从而达到按需内联(低优先级下不内联,高优先级下按需内联)。
#ifdef XXX
#define __myapp_inline __attribute__((always_inline))
#else
#define __myapp_inline
#endif
这段编译输出信息,也提示了内联过程,对于add_many
函数而言,其有16个add_one
函数调用,因此内联了16次。但是实际上num
和i
都是常数,在内联add_many
函数到main
和add_much
函数中时,会自动删去多余的函数,下面是修改部分代码后的编译信息输出和对应的反汇编指令:
修改部分常数值,此时编译器可以自行裁剪dead-code,从而缩减代码体积。
static inline int add_much(int a)
{
a = add_many(a, 1);
a = add_many(a, 1);
return a;
}
int main(void)
{
int a = 10;
a = add_many(a, 4);
return add_much(a);
}
编译信息输出:
ubuntu->gcc-test:$ gcc -o simple-inline.out simple-inline.c -std=c99 -fopt-info-inline -Winline -O1
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:50:9: optimized: Inlining add_one/13 into add_many/14.
simple-inline.c:60:9: optimized: Inlined add_many/49 into add_much/15 which now has time 33.222000 and size 16, net change of +5.
simple-inline.c:59:9: optimized: Inlined add_many/51 into add_much/15 which now has time 38.444000 and size 21, net change of +5.
simple-inline.c:68:12: optimized: Inlined add_much/15 into main/16 which now has time 51.444000 and size 25, net change of -6.
simple-inline.c:67:13: optimized: Inlined add_many/14 into main/16 which now has time 59.717261 and size 44, net change of -115.
最终main函数代码(请注意,其他函数(add_one/add_many/add_much都已经内联成功了)):
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 53 push %rbx
114f: 48 83 ec 08 sub $0x8,%rsp
1153: b9 0b 00 00 00 mov $0xb,%ecx
1158: ba 01 00 00 00 mov $0x1,%edx
115d: 48 8d 1d a0 0e 00 00 lea 0xea0(%rip),%rbx # 2004 <_IO_stdin_used+0x4>
1164: 48 89 de mov %rbx,%rsi
1167: bf 01 00 00 00 mov $0x1,%edi
116c: b8 00 00 00 00 mov $0x0,%eax
1171: e8 da fe ff ff call 1050 <__printf_chk@plt>
1176: b9 0c 00 00 00 mov $0xc,%ecx
117b: ba 02 00 00 00 mov $0x2,%edx
1180: 48 89 de mov %rbx,%rsi
1183: bf 01 00 00 00 mov $0x1,%edi
1188: b8 00 00 00 00 mov $0x0,%eax
118d: e8 be fe ff ff call 1050 <__printf_chk@plt>
1192: b9 0d 00 00 00 mov $0xd,%ecx
1197: ba 03 00 00 00 mov $0x3,%edx
119c: 48 89 de mov %rbx,%rsi
119f: bf 01 00 00 00 mov $0x1,%edi
11a4: b8 00 00 00 00 mov $0x0,%eax
11a9: e8 a2 fe ff ff call 1050 <__printf_chk@plt>
11ae: b9 0e 00 00 00 mov $0xe,%ecx
11b3: ba 04 00 00 00 mov $0x4,%edx
11b8: 48 89 de mov %rbx,%rsi
11bb: bf 01 00 00 00 mov $0x1,%edi
11c0: b8 00 00 00 00 mov $0x0,%eax
11c5: e8 86 fe ff ff call 1050 <__printf_chk@plt>
11ca: 48 8d 2d 48 0e 00 00 lea 0xe48(%rip),%rbp # 2019 <_IO_stdin_used+0x19>
11d1: 48 89 ee mov %rbp,%rsi
11d4: bf 01 00 00 00 mov $0x1,%edi
11d9: b8 00 00 00 00 mov $0x0,%eax
11de: e8 6d fe ff ff call 1050 <__printf_chk@plt>
11e3: b9 0f 00 00 00 mov $0xf,%ecx
11e8: ba 01 00 00 00 mov $0x1,%edx
11ed: 48 89 de mov %rbx,%rsi
11f0: bf 01 00 00 00 mov $0x1,%edi
11f5: b8 00 00 00 00 mov $0x0,%eax
11fa: e8 51 fe ff ff call 1050 <__printf_chk@plt>
11ff: 48 89 ee mov %rbp,%rsi
1202: bf 01 00 00 00 mov $0x1,%edi
1207: b8 00 00 00 00 mov $0x0,%eax
120c: e8 3f fe ff ff call 1050 <__printf_chk@plt>
1211: b9 10 00 00 00 mov $0x10,%ecx
1216: ba 01 00 00 00 mov $0x1,%edx
121b: 48 89 de mov %rbx,%rsi
121e: bf 01 00 00 00 mov $0x1,%edi
1223: b8 00 00 00 00 mov $0x0,%eax
1228: e8 23 fe ff ff call 1050 <__printf_chk@plt>
122d: 48 89 ee mov %rbp,%rsi
1230: bf 01 00 00 00 mov $0x1,%edi
1235: b8 00 00 00 00 mov $0x0,%eax
123a: e8 11 fe ff ff call 1050 <__printf_chk@plt>
123f: b8 10 00 00 00 mov $0x10,%eax
1244: 48 83 c4 08 add $0x8,%rsp
1248: 5b pop %rbx
1249: 5d pop %rbp
124a: c3 ret
可以通过printf
函数的调用次数来简单判断一下,一共有9次调用,符合代码里面的实际调用次数(2 + 2 + 5 = 9)。相对于非内联函数展开,展开的代码减少了每个调用路径上实际的代码路径长度,并且是代码更高效。而在上层表现形式上(C语言编码层级),还体现出抽象统一(add_one函数定义只有一个,但可以根据不同的调用环境生成对应的特定执行代码)。
3. 常量修饰和内联函数(const&inline)
基于上面的讨论可以发现,内联函数减少死亡代码(dead code)和实现特化代码的关键因素是相关的代码运算数据为常数。但是如果只是字面量常数,那作用其实非常有限,总不可能用宏操作去写复杂的代码,那样还不如全部用宏类语言(M4/bison/flex等)实现。
本章主要分析const
常量修饰符和函数内联展开的关系,从而总结出利用const常量达到编译器计算和函数内联展开的技巧。
3.1 编译期计算
编译期计算是一种通用且高效的代码优化方式,在C代码层级,我们需要尽可能维持高度抽象统一,因此往往不会去计算出实际的值(我们需要去抽象计算过程而不是结果,因为结果是特化值,但过程是抽象统一的)。下面是一个典型的例子:
int multiple(int a, int b) { return a * b; }
static __always_inline multiple2(int a, int b) { return a * b; }
#define multiple3(a, b) ((a) * (b))
这里有三个不同的乘法过程定义,第一个是正常的函数定义,能实现一定的抽象程度,但是缺点也很明显,有调用函数,且对于常数的情况下,无法进行编译期计算。但是如果开启了高优化等级编译,那么multiple和multiple2的作用是一致的,在编译期便会确定值,然后直接省略中间的过程。
这里有意思的是multiple3和multiple2的区别,宏定义的形式抽象程度最高,因为都不需要考虑类型定义(变量类型也是一种抽象,而且是高阶抽象)。
有人可能会认为C语言作为典型的面向过程编程语言,为什么需要频繁强调抽象过程?其实,从逻辑的角度来看,抽象过程确定了一个对象的属性和方法,当我们从对象部分数据出发,通过属性和方法便可以得到我们想要结果。面向过程编程是直接把通过属性和方法求解的过程直接写出来,因此往往只是某种情况下的解法。
再来看看multiple3
过程,这部分代码是在更高层级实现对变量类型的屏蔽,如果不考虑不同类型的溢出等错误处理情况,那么multiple3
定义自然更好。multiple2
函数的好处是,类型是可感知的,所以可以直接针对整数类型进行一些错误判断。
再来看看下面几种调用(有5类,变量可以忽略,指针可以无限套娃,但递归逻辑就这三种基础指针):
// (1) 直接填字面量常数(包括宏常量/枚举)
multiple(1, 2);
// (2) 填入const常量
const int k = 2;
multiple(1, k);
// (3) 填入const常量指针(指向常量的指针)
const int *kp1 = &k;
multiple(1, *kp1);
// (4) 填入const指针常量(指向变量的指针常量)
int * const kp2 = &k;
multiple(1, *kp2);
// (5) 填入const常量的const指针常量(指向常量的指针常量)
const int * const kp3 = &k;
multiple(1, *kp3);
变量情况就不用考虑了,**“肯定”**是不能编译期计算的(固定的值也不意味着不能改变,可以通过地址直接改值,对变量做编译期计算优化,带来的问题更多,本文只讨论常量)。
上面的定义还有一个关键的属性:作用域,一般有三种,全局(global)、本地(static)、函数局部(栈变量)。作用域不同,上面呈现出来的规则也不一样。全局和本地的表现一致,即上述5个例子中,只有第3个例子(填入const常量指针(指向常量的指针))无法进行编译期计算。对于函数局部作用域,5个例子都能编译期计算。总结如下:
- const常量编译期计算的作用和字面量常量基本一样,可以通过使用更为复杂的常量数据结构实现编译期计算过程,减少代码运行时的计算量。
- 第4个例子的定义初始化其实有问题,因为指针类型和初始化值的类型不匹配,正常情况是不能这么定义的,会报错。
- 第5个例子是最合适的定义,不仅指针需要是常量,而且指向的值也应该是常量,从而实现常量传递,这里是简单的常量,涉及多维数组定义时,会更加复杂。
- (在某些低版本编译器中)const常量无法应用于全部的数据初始化场景,因此某些情况,需要通过指针来进行数据传递。
3.2 常量分支判断
在上面,揭示了常量在编译期可以直接确定计算,而不需要总是重复计算内容。下面是如何在利用常量分支判断来减少无用dead code。如下一段代码:
static inline int test_const_judge(const int condition)
{
if (condition < 1) {
return 0x2222;
} else if (condition < 128) {
return 0x3333;
} else {
return 0x4444;
}
}
int main(void)
{
int a = 10;
return test_const_judge(a);
}
使用如下的编译选项:
gcc -o simple-inline.out simple-inline.c -std=c99 -fopt-info-inline -Winline -O1
最后生成的代码里面,test_const_judge是内联到main函数里面,并且那些无用的判断代码和指令都被移除了:
00000000000011e8 <main>:
11e8: f3 0f 1e fa endbr64
11ec: b8 33 33 00 00 mov $0x3333,%eax
11f1: c3 ret
可以看到,最终的汇编代码非常简洁,直接把结果返回了。更有效的使用方式是搭配常量数组,实现更高级的编译期数据计算过程。
const int test_data[] = { 0x1111, 0x2222, 0x3333, 0x4444 };
static inline int test_const_judge(const int condition)
{
if (condition >= sizeof(test_data)/sizeof(test_data[0])) {
return 0xffff;
}
return test_data[condition];
}
int main(void)
{
int a = 10;
return test_const_judge(a);
}
这里的例子十分简单,展现了利用const和内联函数实现编译期计算的效果,实际代码里直接取出对应的值,而不需要去判断和取址。反汇编的指令如下:
00000000000011e8 <main>:
11e8: f3 0f 1e fa endbr64
11ec: b8 ff ff 00 00 mov $0xffff,%eax
11f1: c3 ret
3.3 常量函数内联
这是一个进阶的示例,先通过编译器属性设置给add_one
函数生成另外一个别名:
static inline int add_one(int ori)
{
return ori + 1;
}
/* 如果没有static声明, 会生成外部符号 */
static __typeof__(add_one) add_one_2 __attribute__((alias("add_one")));
int main(void)
{
int a = 10;
return add_one_2(a);
}
通过别名来调用函数,同样会被内联,如下:
00000000000011e8 <main>:
11e8: f3 0f 1e fa endbr64
11ec: b8 0b 00 00 00 mov $0xb,%eax
11f1: c3 ret
因此,可以认为编译器对于编译期能确定的函数调用, 都可以进行内联优化,那怕这个函数是通过函数变量指定的。下面是一个复杂的例子:
#include <stdio.h>
static inline int add_one(int ori)
{
return ori + 1;
}
/* clang-format off */
static const struct sss {
__typeof__(&add_one) aaa;
} aaa_ccc[] = {
add_one, add_one, add_one, add_one, add_one, add_one, add_one, add_one,
};
static const struct sss aaa_ccc2[] = {
add_one, add_one, add_one, add_one,
};
static const struct sss *const aaa_ccc_xxx[] = { aaa_ccc2, aaa_ccc, NULL};
#define ss(v) sizeof(v) / sizeof(v[0])
static const int aaa_ccc_sss[] = {ss(aaa_ccc2), ss(aaa_ccc), 0};
#define VPP_REPEAT_1(op) op(1)
#define VPP_REPEAT_2(op) VPP_REPEAT_1(op); op(2)
#define VPP_REPEAT_3(op) VPP_REPEAT_2(op); op(3)
#define VPP_REPEAT_4(op) VPP_REPEAT_3(op); op(4)
#define VPP_REPEAT_5(op) VPP_REPEAT_4(op); op(5)
#define VPP_REPEAT_6(op) VPP_REPEAT_5(op); op(6)
#define VPP_REPEAT_7(op) VPP_REPEAT_6(op); op(7)
#define VPP_REPEAT_8(op) VPP_REPEAT_7(op); op(8)
#define VPP_REPEAT_9(op) VPP_REPEAT_8(op); op(9)
#define VPP_REPEAT_10(op) VPP_REPEAT_9(op); op(10)
#define VPP_REPEAT_11(op) VPP_REPEAT_10(op); op(11)
#define VPP_REPEAT_12(op) VPP_REPEAT_11(op); op(12)
#define VPP_REPEAT_13(op) VPP_REPEAT_12(op); op(13)
#define VPP_REPEAT_14(op) VPP_REPEAT_13(op); op(14)
#define VPP_REPEAT_15(op) VPP_REPEAT_14(op); op(15)
#define VPP_REPEAT_16(op) VPP_REPEAT_15(op); op(16)
#define VPP_REPEAT(op, times) VPP_REPEAT_##times(op)
/* clang-format on */
__always_inline static inline int add_many(int a, int b)
{
#define DO_ONE(i) \
{ \
if (i > aaa_ccc_sss[b]) { \
break; \
} \
a = aaa_ccc_xxx[b][i].aaa(a); \
printf("[%i]add one -> %d .\n", i, a); \
}
do {
VPP_REPEAT(DO_ONE, 16);
} while (0);
printf("Add many over.\n");
return a;
}
static inline int add_much(int a)
{
return add_many(a, 1);
}
int main(void)
{
int a = 10;
a = add_much(a);
return add_many(a, 2);
}
在这个代码里,实际在add_many
中插入的函数是固定数目的,因此最终生成的代码都将内联到main函数中,并且没有多余的函数调用。编译信息输出如下:
ubuntu->gcc-test:$ gcc -o simple-inline.out simple-inline.c -std=c99 -fopt-info-inline -Winline -O1
simple-inline.c:66:5: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:64:9: optimized: Inlining printf/7 into add_many/18 (always_inline).
simple-inline.c:73:12: optimized: Inlining add_many/18 into add_much/19 (always_inline).
simple-inline.c:136:12: optimized: Inlining add_many/18 into main/20 (always_inline).
simple-inline.c:64:9: optimized: Inlined add_one/39 into add_much/19 which now has time 216.000000 and size 70, net change of -2.
simple-inline.c:64:9: optimized: Inlined add_one/40 into add_much/19 which now has time 205.000000 and size 68, net change of -2.
simple-inline.c:64:9: optimized: Inlined add_one/41 into add_much/19 which now has time 194.000000 and size 66, net change of -2.
simple-inline.c:64:9: optimized: Inlined add_one/42 into add_much/19 which now has time 183.000000 and size 64, net change of -2.
simple-inline.c:64:9: optimized: Inlined add_one/43 into add_much/19 which now has time 172.000000 and size 62, net change of -2.
simple-inline.c:64:9: optimized: Inlined add_one/44 into add_much/19 which now has time 161.000000 and size 60, net change of -2.
simple-inline.c:64:9: optimized: Inlined add_one/13 into add_much/19 which now has time 150.000000 and size 58, net change of -6.
simple-inline.c:135:9: optimized: Inlined add_much/19 into main/20 which now has time 161.000000 and size 60, net change of -7.
实际的汇编代码输出如下(只有main函数):
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 53 push %rbx
114f: 48 83 ec 08 sub $0x8,%rsp
1153: b9 0b 00 00 00 mov $0xb,%ecx
1158: ba 01 00 00 00 mov $0x1,%edx
115d: 48 8d 1d a0 0e 00 00 lea 0xea0(%rip),%rbx # 2004 <_IO_stdin_used+0x4>
1164: 48 89 de mov %rbx,%rsi
1167: bf 01 00 00 00 mov $0x1,%edi
116c: b8 00 00 00 00 mov $0x0,%eax
1171: e8 da fe ff ff call 1050 <__printf_chk@plt>
1176: b9 0c 00 00 00 mov $0xc,%ecx
117b: ba 02 00 00 00 mov $0x2,%edx
1180: 48 89 de mov %rbx,%rsi
1183: bf 01 00 00 00 mov $0x1,%edi
1188: b8 00 00 00 00 mov $0x0,%eax
118d: e8 be fe ff ff call 1050 <__printf_chk@plt>
1192: b9 0d 00 00 00 mov $0xd,%ecx
1197: ba 03 00 00 00 mov $0x3,%edx
119c: 48 89 de mov %rbx,%rsi
119f: bf 01 00 00 00 mov $0x1,%edi
11a4: b8 00 00 00 00 mov $0x0,%eax
11a9: e8 a2 fe ff ff call 1050 <__printf_chk@plt>
11ae: b9 0e 00 00 00 mov $0xe,%ecx
11b3: ba 04 00 00 00 mov $0x4,%edx
11b8: 48 89 de mov %rbx,%rsi
11bb: bf 01 00 00 00 mov $0x1,%edi
11c0: b8 00 00 00 00 mov $0x0,%eax
11c5: e8 86 fe ff ff call 1050 <__printf_chk@plt>
11ca: b9 0f 00 00 00 mov $0xf,%ecx
11cf: ba 05 00 00 00 mov $0x5,%edx
11d4: 48 89 de mov %rbx,%rsi
11d7: bf 01 00 00 00 mov $0x1,%edi
11dc: b8 00 00 00 00 mov $0x0,%eax
11e1: e8 6a fe ff ff call 1050 <__printf_chk@plt>
11e6: b9 10 00 00 00 mov $0x10,%ecx
11eb: ba 06 00 00 00 mov $0x6,%edx
11f0: 48 89 de mov %rbx,%rsi
11f3: bf 01 00 00 00 mov $0x1,%edi
11f8: b8 00 00 00 00 mov $0x0,%eax
11fd: e8 4e fe ff ff call 1050 <__printf_chk@plt>
1202: b9 11 00 00 00 mov $0x11,%ecx
1207: ba 07 00 00 00 mov $0x7,%edx
120c: 48 89 de mov %rbx,%rsi
120f: bf 01 00 00 00 mov $0x1,%edi
1214: b8 00 00 00 00 mov $0x0,%eax
1219: e8 32 fe ff ff call 1050 <__printf_chk@plt>
121e: bf 11 00 00 00 mov $0x11,%edi
1223: b8 00 00 00 00 mov $0x0,%eax
1228: ff d0 call *%rax
122a: 89 c5 mov %eax,%ebp
122c: 89 c1 mov %eax,%ecx
122e: ba 08 00 00 00 mov $0x8,%edx
1233: 48 89 de mov %rbx,%rsi
1236: bf 01 00 00 00 mov $0x1,%edi
123b: b8 00 00 00 00 mov $0x0,%eax
1240: e8 0b fe ff ff call 1050 <__printf_chk@plt>
1245: 48 8d 1d cd 0d 00 00 lea 0xdcd(%rip),%rbx # 2019 <_IO_stdin_used+0x19>
124c: 48 89 de mov %rbx,%rsi
124f: bf 01 00 00 00 mov $0x1,%edi
1254: b8 00 00 00 00 mov $0x0,%eax
1259: e8 f2 fd ff ff call 1050 <__printf_chk@plt>
125e: 48 89 de mov %rbx,%rsi
1261: bf 01 00 00 00 mov $0x1,%edi
1266: b8 00 00 00 00 mov $0x0,%eax
126b: e8 e0 fd ff ff call 1050 <__printf_chk@plt>
1270: 89 e8 mov %ebp,%eax
1272: 48 83 c4 08 add $0x8,%rsp
1276: 5b pop %rbx
1277: 5d pop %rbp
1278: c3 ret
这里有10个printf函数,符合实际数组里面的函数数量和对应的索引关系,多余的无用函数代码都被裁剪掉了。类似的操作还有很多,这里就不一一列举了,函数是否内联,最好的方法还是通过汇编代码来确定。
4.总结
对于函数内联来说,有时候即使使用常数,也无法完成预期的常数展开/计算/裁剪,那此时可以改变函数参数的定义方式如下:
int func (const int k) { aaa[k](...) };
如果上面的aaa[k]
函数无法展开或者在编译期确定,不妨修改aaa
定义为常量,以及修改定义如下:
int func (const __typeof__(aaa[0]) func) { func(...) };
通过上面的方式修改,可以有效解决部分常量数组无法展开的情况,对于内联函数来说,参数多一些无所谓,因为内联之后,是没有参数进出栈的开销的。
更多时候也需要灵活处理问题,比如使用宏和内联汇编,更多有关内联函数的细节,则需要在日常使用中进一步总结。