(如何)我可以内联特定的函数调用吗?

假设我有一个在程序的多个部分中调用的函数。 我们还要说我对该函数的特定调用是在一个性能极其敏感的代码段中(例如,循环迭代数千万次,每微秒计数一次)。 有没有办法可以强制编译器(在我的情况下为gcc )内联单个特定函数调用,而不是内联其他函数?

编辑:让我完全清楚:这个问题不是强迫 gcc(或任何其他编译器)内联所有函数调用; 相反,它是关于请求编译器内联对函数的特定调用

在C(而不是C ++)中,没有标准的方法来建议应该内联函数。 这只是特定于供应商的扩展。

但是你指定它,据我所知,编译器将始终尝试内联每个实例,因此只使用该函数一次:

原版的:

  int MyFunc() { /* do stuff */ } 

改成:

  inline int MyFunc_inlined() { /* do stuff */ } int MyFunc() { return MyFunc_inlined(); } 

现在,在您想要内联的位置,使用MyFunc_inlined()

注意:上面的“inline”关键字只是gcc用于强制内联的语法的占位符。 如果要信任H2CO3的删除答案,那将是:

 static inline __attribute__((always_inline)) int MyFunc_inlined() { /* do stuff */ } 

可以启用每个翻译单元的内联(但不是每次调用)。 虽然这不是问题的答案,而且是一个丑陋的技巧,但它符合C标准,并且作为相关内容可能很有趣。

诀窍是在不需要内extern inline地方使用extern定义,并在需要内extern inline地方使用extern inline联。

例:

 $ cat func.h int func(); $ cat func.c int func() { return 10; } $ cat func_inline.h extern inline int func() { return 5; } $ cat main.c #include  #ifdef USE_INLINE # include "func_inline.h" #else # include "func.h" #endif int main() { printf("%d\n", func()); return 0; } $ gcc main.c func.c && ./a.out 10 // non-inlined version $ gcc main.c func.c -DUSE_INLINE && ./a.out 10 // non-inlined version $ gcc main.c func.c -DUSE_INLINE -O2 && ./a.out 5 // inlined! 

您还可以在GCC中使用非标准属性(例如__attribute__(always_inline)) )进行extern inline定义,而不是依赖于-O2

顺便说一句,这个技巧用于glibc 。

在C中强制内联函数的传统方法是根本不使用函数,而是使用像宏这样的函数。 此方法将始终内联函数,但是像宏这样的函数存在一些问题。 例如:

 #define ADD(x, y) ((x) + (y)) printf("%d\n", ADD(2, 2)); 

还有inline关键字,它在C99标准中被添加到C. 值得注意的是,Microsoft的Visual C编译器不支持C99,因此您不能使用内联那个(悲惨的)编译器。 内联仅向编译器提示您希望函数内联 – 它不保证它。

GCC有一个扩展,它要求编译器内联函数。

 inline __attribute__((always_inline)) int add(int x, int y) { return x + y; } 

为了使这个更干净,你可能想要使用宏:

 #define ALWAYS_INLINE inline __attribute__((always_inline)) ALWAYS_INLINE int add(int x, int y) { return x + y; } 

我不知道在某些调用中强制内联函数的直接方法。 但是你可以结合这样的技术:

 #define ALWAYS_INLINE inline __attribute__((always_inline)) #define ADD(x, y) ((x) + (y)) ALWAYS_INLINE int always_inline_add(int x, int y) { return ADD(x, y); } int normal_add(int x, int y) { return ADD(x, y); } 

或者,你可以这样:

 #define ADD(x, y) ((x) + (y)) int add(int x, int y) { return ADD(x, y); } int main() { printf("%d\n", ADD(2,2)); // always inline printf("%d\n", add(2,2)); // normal function call return 0; } 

另请注意,强制函数内联可能不会使代码更快。 内联函数会导致生成更大的代码,这可能会导致更多的缓存未命中。 我希望有所帮助。

答案取决于您的function,您要求的内容以及function的性质。 你最好的选择是:

  • 告诉编译器你想要它内联
  • 使函数静态(小心extern,因为它的语义在某些模式下在gcc中稍有变化)
  • 设置编译器选项以通知要内联的优化程序,并相应地设置内联限制
  • 打开任何无法在编译器上内联警告
  • validation输出(您可以检查生成的汇编程序)该函数是否为内联函数。

编译器提示

这里的答案仅涵盖内联的一个方面,语言提示编译器。 当标准说:

使函数成为内联函数表明对函数的调用尽可能快。 这些建议有效的程度是实施定义的

其他更强的提示可能就是这种情况,例如:

  • GNU的__attribute__((always_inline)) :通常,除非指定了优化,否则不会内联函数。 对于内联声明的函数,即使未指定优化级别,此属性也会内联函数。
  • Microsoft的__forceinline :__ forceinline关键字会覆盖成本/收益分析,而是依赖于程序员的判断。 使用__forceinline时要小心。 不加选择地使用__forceinline可能会导致更大的代码,只会带来边际性能提升,或者在某些情况下甚至会导致性能损失(例如,由于更大的可执行文件的分页增加)。

即使这两者都依赖于内联是可能的,而且关键在于编译器标志。 要使用内联函数,还需要了解编译器的优化设置。

值得一提的是,内联也可以用于为您所在的编译单元提供现有函数的替换。这可以在大概答案对您的算法足够好时使用,或者可以以更快的方式实现结果与本地数据结构。

内联定义提供了外部定义的替代方案,翻译器可以使用该定义在同一翻译单元中实现对该function的任何调用。 未指定对函数的调用是使用内联定义还是使用外部定义。

某些function无法内联

例如,对于GNU编译器,无法内联的函数是:

请注意,函数定义中的某些用法可能使其不适合内联替换。 这些用法包​​括:可变参数函数,alloca的使用,可变长度数据类型的使用(请参阅可变长度),使用计算goto(请参阅标签作为值),使用非本地goto和嵌套函数(请参阅嵌套函数)。 当标记为内联的函数无法替换时,使用-Winline会发出警告,并说明失败的原因。

因此,即使always_inline也可能无法达到您的预期。

编译器选项

使用C99的内联提示将依赖于您指示编译器您正在寻找的内联行为。

例如GCC有:

-fno-inline-finline-small-functions-findirect-inlining-finline-functions-finline-functions-called-once-fearly-inlining-finline-limit=n

Microsoft编译器还有一些选项可以指示内联的有效性。 一些编译器还允许优化以考虑运行配置文件。

我认为值得在更广泛的程序优化环境中进行内联。

防止内联

您提到您不希望内联某些function。 这可以通过设置类似__attribute__((always_inline))而不打开优化器来完成。 但是你可能会想要优化器。 这里的一个选择是提示你不要它: __attribute__ ((noinline)) 。 但为什么会这样呢?

其他forms的优化

您还可以考虑如何重构循环并避免分支。 分支预测可以产生戏剧性的效果。 有关此问题的有趣讨论请参阅: 为什么处理排序数组比处理未排序数组更快?

然后你也可以展开更小的内部循环并查看不变量。

有一个内核源代码以非常有趣的方式使用#define来定义具有相同主体的几个不同的命名函数。 这解决了维护两个不同function的问题 。 (我忘了它是哪一个……)。 我的想法是基于同样的原则。

使用定义的方法是您将在您需要的编译单元上定义内联函数。 为了演示方法,我将使用一个简单的函数:

 int add(int a, int b); 

它的工作方式如下:在头文件中创建一个函数生成器#define ,并声明该函数的正常版本的函数原型( 未内联的函数)。

然后声明两个单独的函数生成器 ,一个用于普通函数,另一个用于内联函数。 您声明为static __inline__的内联函数。 当您需要在其中一个文件中调用内联函数时,可以使用生成器定义来获取它的源代码。 在所有其他文件中,您需要使用普通函数,您只需在原型中包含标题。

代码测试了:

 Intel(R) Core(TM) i5-3330 CPU @ 3.00GHz Kernel Version: 3.16.0-49-generic GCC 4.8.4 

代码值超过千言万语,因此:

文件层次结构

 + | Makefile | add.h | add.c | loop.c | loop2.c | loop3.c | loops.h | main.c 

add.h

 #define GENERATE_ADD(type, prefix) \ type int prefix##add(int a, int b) { return a + b; } #define DEFINE_ADD() GENERATE_ADD(,) #define DEFINE_INLINE_ADD() GENERATE_ADD(static __inline__, inline_) int add(int, int); 

这看起来不太好,但却削减了维护两种不同function的工作。 该函数在GENERATE_ADD(type,prefix)宏中完全定义,因此如果您需要更改该函数,则更改此宏并更改其他所有内容。

接下来, DEFINE_ADD()调用DEFINE_ADD()以生成正常版本的addDEFINE_INLINE_ADD()将允许您访问名为inline_add的函数,该函数具有普通add函数相同的签名 ,但它具有不同的名称( inline_前缀)。

注意:我在使用-O3标志时没有使用__attribute((always_inline))____inline__完成了这项工作。 但是,如果您不想使用-O3 ,请使用:

 #define DEFINE_INLINE_ADD() GENERATE_ADD(static __inline__ __attribute__((always_inline)), inline_) 

add.c

 #include "add.h" DEFINE_ADD() 

简单调用DEFINE_ADD()宏生成器。 这将声明函数的正常版本(不会内联的函数)。

loop.c中

 #include  #include "add.h" DEFINE_INLINE_ADD() int loop(void) { register int i; for (i = 0; i < 100000; i++) printf("%d\n", inline_add(i + 1, i + 2)); return 0; } 

loop.c您可以看到对DEFINE_INLINE_ADD()的调用。 这使该函数可以访问inline_add函数。 编译时,所有inline_add函数都将内联。

loop2.c

 #include  #include "add.h" int loop2(void) { register int i; for (i = 0; i < 100000; i++) printf("%d\n", add(i + 1, i + 2)); return 0; } 

这是为了表明您可以正常使用其他文件中的正常add版本。

loop3.c

 #include  #include "add.h" DEFINE_INLINE_ADD() int loop3(void) { register int i; printf ("add: %d\n", add(2,3)); printf ("add: %d\n", add(4,5)); for (i = 0; i < 100000; i++) printf("%d\n", inline_add(i + 1, i + 2)); return 0; } 

这是为了表明您可以在同一个编译单元中使用这两个函数,但其​​中一个函数将被内联,而另一个函数将不会(有关详细信息,请参阅GDB版本 )。

loops.h

 /* prototypes for main */ int loop (void); int loop2 (void); int loop3 (void); 

main.c中

 #include  #include  #include "add.h" #include "loops.h" int main(void) { printf("%d\n", add(1,2)); printf("%d\n", add(2,3)); loop(); loop2(); loop3(); return 0; } 

Makefile文件

 CC=gcc CFLAGS=-Wall -pedantic --std=c11 main: add.o loop.o loop2.o loop3.o main.o ${CC} -o $@ $^ ${CFLAGS} add.o: add.c ${CC} -c $^ ${CFLAGS} loop.o: loop.c ${CC} -c $^ -O3 ${CFLAGS} loop2.o: loop2.c ${CC} -c $^ ${CFLAGS} loop3.o: loop3.c ${CC} -c $^ -O3 ${CFLAGS} 

如果使用__attribute__((always_inline)) ,可以将Makefile更改为:

 CC=gcc CFLAGS=-Wall -pedantic --std=c11 main: add.o loop.o loop2.o loop3.o main.o ${CC} -o $@ $^ ${CFLAGS} %.o: %.c ${CC} -c $^ ${CFLAGS} 

汇编

 $ make gcc -c add.c -Wall -pedantic --std=c11 gcc -c loop.c -O3 -Wall -pedantic --std=c11 gcc -c loop2.c -Wall -pedantic --std=c11 gcc -c loop3.c -O3 -Wall -pedantic --std=c11 gcc -Wall -pedantic --std=c11 -c -o main.o main.c gcc -o main add.o loop.o loop2.o loop3.o main.o -Wall -pedantic --std=c11 

拆卸

 $ gdb main (gdb) disass add 0x000000000040059d <+0>: push %rbp 0x000000000040059e <+1>: mov %rsp,%rbp 0x00000000004005a1 <+4>: mov %edi,-0x4(%rbp) 0x00000000004005a4 <+7>: mov %esi,-0x8(%rbp) 0x00000000004005a7 <+10>:mov -0x8(%rbp),%eax 0x00000000004005aa <+13>:mov -0x4(%rbp),%edx 0x00000000004005ad <+16>:add %edx,%eax 0x00000000004005af <+18>:pop %rbp 0x00000000004005b0 <+19>:retq (gdb) disass loop 0x00000000004005c0 <+0>: push %rbx 0x00000000004005c1 <+1>: mov $0x3,%ebx 0x00000000004005c6 <+6>: nopw %cs:0x0(%rax,%rax,1) 0x00000000004005d0 <+16>:mov %ebx,%edx 0x00000000004005d2 <+18>:xor %eax,%eax 0x00000000004005d4 <+20>:mov $0x40079d,%esi 0x00000000004005d9 <+25>:mov $0x1,%edi 0x00000000004005de <+30>:add $0x2,%ebx 0x00000000004005e1 <+33>:callq 0x4004a0 <__printf_chk@plt> 0x00000000004005e6 <+38>:cmp $0x30d43,%ebx 0x00000000004005ec <+44>:jne 0x4005d0  0x00000000004005ee <+46>:xor %eax,%eax 0x00000000004005f0 <+48>:pop %rbx 0x00000000004005f1 <+49>:retq (gdb) disass loop2 0x00000000004005f2 <+0>: push %rbp 0x00000000004005f3 <+1>: mov %rsp,%rbp 0x00000000004005f6 <+4>: push %rbx 0x00000000004005f7 <+5>: sub $0x8,%rsp 0x00000000004005fb <+9>: mov $0x0,%ebx 0x0000000000400600 <+14>:jmp 0x400625  0x0000000000400602 <+16>:lea 0x2(%rbx),%edx 0x0000000000400605 <+19>:lea 0x1(%rbx),%eax 0x0000000000400608 <+22>:mov %edx,%esi 0x000000000040060a <+24>:mov %eax,%edi 0x000000000040060c <+26>:callq 0x40059d  0x0000000000400611 <+31>:mov %eax,%esi 0x0000000000400613 <+33>:mov $0x400794,%edi 0x0000000000400618 <+38>:mov $0x0,%eax 0x000000000040061d <+43>:callq 0x400470  0x0000000000400622 <+48>:add $0x1,%ebx 0x0000000000400625 <+51>:cmp $0x1869f,%ebx 0x000000000040062b <+57>:jle 0x400602  0x000000000040062d <+59>:mov $0x0,%eax 0x0000000000400632 <+64>:add $0x8,%rsp 0x0000000000400636 <+68>:pop %rbx 0x0000000000400637 <+69>:pop %rbp 0x0000000000400638 <+70>:retq (gdb) disass loop3 0x0000000000400640 <+0>: push %rbx 0x0000000000400641 <+1>: mov $0x3,%esi 0x0000000000400646 <+6>: mov $0x2,%edi 0x000000000040064b <+11>:mov $0x3,%ebx 0x0000000000400650 <+16>:callq 0x40059d  0x0000000000400655 <+21>:mov $0x400798,%esi 0x000000000040065a <+26>:mov %eax,%edx 0x000000000040065c <+28>:mov $0x1,%edi 0x0000000000400661 <+33>:xor %eax,%eax 0x0000000000400663 <+35>:callq 0x4004a0 <__printf_chk@plt> 0x0000000000400668 <+40>:mov $0x5,%esi 0x000000000040066d <+45>:mov $0x4,%edi 0x0000000000400672 <+50>:callq 0x40059d  0x0000000000400677 <+55>:mov $0x400798,%esi 0x000000000040067c <+60>:mov %eax,%edx 0x000000000040067e <+62>:mov $0x1,%edi 0x0000000000400683 <+67>:xor %eax,%eax 0x0000000000400685 <+69>:callq 0x4004a0 <__printf_chk@plt> 0x000000000040068a <+74>:nopw 0x0(%rax,%rax,1) 0x0000000000400690 <+80>:mov %ebx,%edx 0x0000000000400692 <+82>:xor %eax,%eax 0x0000000000400694 <+84>:mov $0x40079d,%esi 0x0000000000400699 <+89>:mov $0x1,%edi 0x000000000040069e <+94>:add $0x2,%ebx 0x00000000004006a1 <+97>:callq 0x4004a0 <__printf_chk@plt> 0x00000000004006a6 <+102>:cmp $0x30d43,%ebx 0x00000000004006ac <+108>:jne 0x400690  0x00000000004006ae <+110>:xor %eax,%eax 0x00000000004006b0 <+112>:pop %rbx 0x00000000004006b1 <+113>:retq 

符号表

 $ objdump -t main | grep add 0000000000000000 l df *ABS* 0000000000000000 add.c 000000000040059d g F .text 0000000000000014 add $ objdump -t main | grep loop 0000000000000000 l df *ABS* 0000000000000000 loop.c 0000000000000000 l df *ABS* 0000000000000000 loop2.c 0000000000000000 l df *ABS* 0000000000000000 loop3.c 00000000004005c0 g F .text 0000000000000032 loop 00000000004005f2 g F .text 0000000000000047 loop2 0000000000400640 g F .text 0000000000000072 loop3 $ objdump -t main | grep main main: file format elf64-x86-64 0000000000000000 l df *ABS* 0000000000000000 main.c 0000000000000000 F *UND* 0000000000000000 __libc_start_main@@GLIBC_2.2.5 00000000004006b2 g F .text 000000000000005a main $ objdump -t main | grep inline $ 

嗯,就是这样。 经过3个小时的敲击键盘试图找出它,这是我能想到的最好的。 随意指出任何错误,我真的很感激。 我对这个特定的内联函数调用非常感兴趣。

如果您不介意为同一个函数使用两个名称,则可以在函数周围创建一个小包装器,以“阻止”always_inline属性影响每个调用。 在我的示例中, loop_inlined将是您在性能关键部分中使用的名称,而plain loop将在其他任何地方使用。

inline.h

 #include  static inline int loop_inlined() __attribute__((always_inline)); int loop(); static inline int loop_inlined() { int n = 0, i; for(i = 0; i < 10000; i++) n += rand(); return n; } 

inline.c

 #include "inline.h" int loop() { return loop_inlined(); } 

main.c中

 #include "inline.h" #include  int main(int argc, char *argv[]) { printf("%d\n", loop_inlined()); printf("%d\n", loop()); return 0; } 

无论优化级别如何,这都有效。 使用英特尔的gcc inline.c main.c进行编译得出:

 4011e6: c7 44 24 18 00 00 00 movl $0x0,0x18(%esp) 4011ed: 00 4011ee: eb 0e jmp 4011fe <_main+0x2e> 4011f0: e8 5b 00 00 00 call 401250 <_rand> 4011f5: 01 44 24 1c add %eax,0x1c(%esp) 4011f9: 83 44 24 18 01 addl $0x1,0x18(%esp) 4011fe: 81 7c 24 18 0f 27 00 cmpl $0x270f,0x18(%esp) 401205: 00 401206: 7e e8 jle 4011f0 <_main+0x20> 401208: 8b 44 24 1c mov 0x1c(%esp),%eax 40120c: 89 44 24 04 mov %eax,0x4(%esp) 401210: c7 04 24 60 30 40 00 movl $0x403060,(%esp) 401217: e8 2c 00 00 00 call 401248 <_printf> 40121c: e8 7f ff ff ff call 4011a0 <_loop> 401221: 89 44 24 04 mov %eax,0x4(%esp) 401225: c7 04 24 60 30 40 00 movl $0x403060,(%esp) 40122c: e8 17 00 00 00 call 401248 <_printf> 

前7个指令是内联调用,常规调用稍后发出5个指令。

这是一个建议,在单独的头文件中写入代码的主体。 将头文件包含在必须内联的位置,并包含在C文件中用于其他调用的正文中。

 void demo(void) { #include myBody.h } importantloop { // code #include myBody.h // code } 

我假设你的函数是一个小的,因为你想内联它,如果是这样,为什么你不在asm中写它?

至于仅内联对函数的特定调用,我认为不存在为您执行此任务的任务。 一旦函数被声明为内联,并且编译器将为您内联它,它将在它看到对该函数的调用的任何地方执行它。