C之(9)函数内联(inline)深入分析

2023-11-19

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_oneadd_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次。但是实际上numi都是常数,在内联add_many函数到mainadd_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(...) };

通过上面的方式修改,可以有效解决部分常量数组无法展开的情况,对于内联函数来说,参数多一些无所谓,因为内联之后,是没有参数进出栈的开销的

更多时候也需要灵活处理问题,比如使用宏和内联汇编,更多有关内联函数的细节,则需要在日常使用中进一步总结。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C之(9)函数内联(inline)深入分析 的相关文章

  • 阿里云大佬告诉你为什么学不会设计模式,归根到底还是方法不对

    最近总有读者在后台跟我说 工作几年 自己的代码质量似乎没有什么提升 我觉得他的情况非常典型 很多人应该或多或少都有过类似的经历 毕业几年 几乎一直在做复制黏贴的工作 偶尔会遇到原有业务扩展的需求 想简单应付一下完事的话 也不难 无非就是多加
  • 查询水果价格 (15分)

    给定四种水果 分别是苹果 apple 梨 pear 桔子 orange 葡萄 grape 单价分别对应为3 00元 公斤 2 50元 公斤 4 10元 公斤 10 20元 公斤 首先在屏幕上显示以下菜单 1 apple 2 pear 3 o

随机推荐

  • Hadoop学习——MapReduce的简单介绍及执行步骤

    MapReduce概述 MapReduce是一个分布式的计算框架 编程模型 最初由由谷歌的工程师开发 基于GFS的分布式计算框架 后来Cutting根据 Google Mapreduce 设计了基于HDFS的Mapreduce分布式计算框架
  • IDEA修改内存未生效原因和解决

    修改IDEA安装目录下的idea64 exe vmoptions server Xms1024m Xmx2048m XX ReservedCodeCacheSize 2048m 发现IDEA的内存修改并未生效 右下角显示依然是974M 原因
  • windows下进程间通信的(13种方法)

    Windows进程间的通信 迎风的祺 博客园 windows下进程间通信的 13种方法 phymat nico的专栏 CSDN博客 windows进程间通信
  • eclipse报错 parameterized types are only available if source level is 1.5 or greater

    preface 好久没有更新 blog 了 最近在 写新的项目 今天在用eclipse 出现了之前遇到的问题 这里记录下 问题 eclipse 控制台 报错 parameterized types are only available if
  • CUDA+OPENCV+PYTHON tensorflow 源码环境搭建

    CUDA OPENCV PYTHON tensorflow 源码环境搭建 接上文caffe环境安装 https blog csdn net u012350025 article details 104589982 主机环境ubuntu18
  • 基于C++的带权无向图的实现 (五)- 连通图和连通分量

    该系列文章是本人整理的有关带权无向图的数据结构和算法的分析与实现 若要查看源码可以访问我的github仓库 如有问题或者建议欢迎各位指出 目录 基于C 的带权无向图的实现 一 数据结构 基于C 的带权无向图的实现 二 遍历算法 基于C 的带
  • 台式计算机怎么看有没有开独显,台式机独立显卡怎么样打开

    给台式机插入了独立显卡 但不会打开怎么办呢 下面由学习啦小编给你做出详细的台式机独立显卡打开方法介绍 希望对你有帮助 台式机独立显卡打开方法一 把显示器的数据连接线接到独立显卡上 用的就是独立显卡 把显示器的数据线连接在主板的显示输出口上
  • MyEclipse8.5的安装过程

    1 双击进行安装 点Next接受 选择好路径后 等待安装完毕 2 选择路径 3 进入工作台如下图 4 配置Tomcat 工具栏 Window Preferences 5 Myeclipse Servers Tomcat 选择版本 勾选Ena
  • 2029:【例4.15】水仙花数

    2029 例4 15 水仙花数 时间限制 1000 ms 内存限制 65536 KB 提交数 1247 通过数 720 题目描述 求100 999中的水仙花数 若三位数ABC ABC A3 B3 C3 则称ABC为水仙花数 例如153 13
  • springboot2.x默认采用cglib代理,以及配置jdk动态代理的方法

    众所周知 springboot开启AOP需要在启动类加上注解 EnableAspectJAutoProxy 但开发过程中发现即使不加 EnableAspectJAutoProxy 注解 bean还是被代理过 而且是Cglib代理对象 此时在
  • win32下Qt5BLE蓝牙开发笔记

    BLE简介 BLE蓝牙是蓝牙2 0以上的蓝牙模块 经典蓝牙是蓝牙2 0以下的蓝牙 蓝牙分为客户端和服务器两端 经典蓝牙可以通过socket编程进行客户端与服务器之间的通信 与网络socket相似 BLE蓝牙则无法使用这种方式进行通信 BLE
  • ICASSP 2023说话人识别方向论文合集

    今年入选 ICASSP 2023 的论文中 说话人识别 声纹识别 方向约有64篇 初步划分为Speaker Verification 31篇 Speaker Recognition 9篇 Speaker Diarization 17篇 An
  • 算法竞赛当中的思考方法——方法论篇。

    方法论 万物皆朴素的第一性原理 几乎任何领域的任何问题的解决方案 都可以看作是 某个结构上的朴素方法的优化 计算机只能处理规模有限的问题 在给定规模且不考虑效率的情况下 问题一定存在朴素解法 具体手段有直接模拟 利用bit枚举 各种搜索算法
  • Spring Cloud面试8连问,谁顶得住?

    问题一 什么是 Spring Cloud Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序 提供与外部系统的集成 Spring cloud Task 一个生命周期短暂的微服务框架 用于
  • 数据结构-带头双向循环链表的基本实现(C语言,简单易懂,含全部代码)

    链表的概念和结构 概念 链表是一种物理存储结构上非连续 非顺序的存储结构 数据元素的逻辑顺序是通过链表中的指针链接次序实现的 结构 实际中链表的结构非常多样 以下情况组合起来就有8种链表结构 1 单向 双向 2 带头 不带头 3 循环 非循
  • java逆序输出一个整数_Java实现整数的逆序输出(三种方法)

    Java实现整数的逆序输出和C语言相似 下面我介绍三种方法 第一种 无限制整数的逆序输出 import java util Scanner class Cycle01 public static void main String args
  • Tulsi编译失败问题解决

    Tulsi编译失败 Xcode12 4 bazel 4 0 brew 20210218 tulsi最新 解决办法 跑了 usr local Cellar python 2 将这个目录去掉或者改名字为不可用 然后系统默认跑了python3就好
  • Qt—帮助系统

    一个完善的应用程序应该提供尽可能丰富的帮助信息 Qt中可以使用工具提示 状态提示以及 What s This 等简单的帮助提示 也可以使用Qt Assistant来提供强大的在线帮助 简单的帮助提示 已经讲到了工具提示和状态提示 这里简单介
  • java实现第五届蓝桥杯排列序数

    排列序数 如果用a b c d这4个字母组成一个串 有4 24种 如果把它们排个序 每个串都对应一个序号 abcd 0 abdc 1 acbd 2 acdb 3 adbc 4 adcb 5 bacd 6 badc 7 bcad 8 bcda
  • C之(9)函数内联(inline)深入分析

    C之 9 函数内联 inline 深入分析 Author Once Day Date 2023年8月9日 漫漫长路 有人对你微笑过嘛 参考引用文档 Using the GNU Compiler Collection GCC Inline 文