假设我有一个在程序的多个部分中被调用的函数。还假设我对该函数有一个特定的调用,该函数位于对性能极为敏感的代码部分(例如,一个迭代数千万次且每一微秒都很重要的循环)。有没有一种方法可以强制编译器(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
在您不想内联和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 <stdio.h>
#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!
您还可以使用非标准属性(例如__attribute__(always_inline))
在 GCC 中)进行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;
}
另外,请注意,强制内联函数可能不会使您的代码更快。内联函数会导致生成更大的代码,这可能会导致更多的缓存未命中。我希望这会有所帮助。
答案是它取决于您的功能、您的要求以及您的功能的性质。你最好的选择是:
编译器提示
这里的答案仅涵盖内联的一方面,即编译器的语言提示。当标准说:
使函数成为内联函数意味着对函数的调用尽可能快。此类建议的有效程度由实施定义
其他更强的提示可能就是这种情况,例如:
__attribute__((always_inline))
:通常,除非指定优化,否则函数不会内联。对于声明为内联的函数,即使未指定优化级别,此属性也会内联函数。__forceinline
: __forceinline 关键字覆盖了成本/收益分析,而是依赖于程序员的判断。使用 __forceinline 时要小心。不加选择地使用 __forceinline 可能会导致更大的代码,而只能获得边际性能增益,或者在某些情况下甚至会导致性能损失(例如,由于更大的可执行文件的分页增加)。甚至这两者都依赖于可能的内联,并且关键是依赖于编译器标志。要使用内联函数,您还需要了解编译器的优化设置。
值得一提的是,内联也可用于为您所在的编译单元提供现有函数的替换。当近似答案对您的算法足够好时,可以使用此方法,或者可以以更快的方式获得结果与本地数据结构。
内联定义提供了外部定义的替代方案,翻译器可以使用它来实现对同一翻译单元中函数的任何调用。未指定对函数的调用是使用内联定义还是外部定义。
有些函数不能内联
例如,对于不能内联的 GNU 编译器函数有:
请注意,函数定义中的某些用法可能使其不适合内联替换。这些用法包括:可变参数函数、alloca 的使用、可变长度数据类型的使用(请参阅可变长度)、计算 goto 的使用(请参阅作为值的标签)、非局部 goto 的使用和嵌套函数(请参阅嵌套函数)。当标记为 inline 的函数无法替换时,使用 -Winline 会发出警告,并给出失败的原因。
因此,甚至always_inline
可能无法达到您的预期。
编译器选项
使用 C99 的内联提示将依赖于您向编译器指示您正在寻找的内联行为。
例如 GCC 有:
-fno-inline
, -finline-small-functions
, -findirect-inlining
, -finline-functions
, -finline-functions-called-once
, -fearly-inlining
,-finline-limit=n
Microsoft 编译器还具有指示内联有效性的选项。一些编译器还允许优化考虑运行配置文件。
我确实认为在更广泛的程序优化背景下内联是值得的。
防止内联
您提到您不希望内联某些功能。这可以通过设置类似__attribute__((always_inline))
而不打开优化器来完成。但是,您可能会想要优化器。这里的一种选择是暗示你不想要它: __attribute__ ((noinline))
. 但为什么会这样呢?
其他形式的优化
您还可以考虑如何重组循环并避免分支。分支预测可以产生巨大的影响。有关这方面的有趣讨论,请参见:为什么处理排序数组比处理未排序数组更快?
然后,您还可以展开更小的内部循环并查看不变量。
如果你不介意同一个函数有两个名字,你可以在你的函数周围创建一个小包装来“阻止”always_inline 属性影响每个调用。在我的示例中,loop_inlined
将是您将在性能关键部分使用的名称,而 plainloop
将在其他任何地方使用。
#include <stdlib.h>
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;
}
#include "inline.h"
int loop() {
return loop_inlined();
}
#include "inline.h"
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("%d\n", loop_inlined());
printf("%d\n", loop());
return 0;
}
无论优化级别如何,这都有效。gcc inline.c main.c
在 Intel 上编译给出:
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 条指令之后发生。
有一个内核源代码#define
以一种非常有趣的方式使用 s 来定义几个具有相同 body 的不同命名函数。这解决了需要维护两个不同功能的问题。(我忘记是哪一个了……)。我的想法是基于同样的原则。
使用定义的方法是您将在您需要的编译单元上定义内联函数。为了演示该方法,我将使用一个简单的函数:
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
#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);
这看起来不太好,但减少了维护两个不同功能的工作。该函数在宏中完全定义GENERATE_ADD(type,prefix)
,因此如果您需要更改函数,则更改此宏,其他所有内容都会更改。
接下来,DEFINE_ADD()
将调用 fromadd.c
以生成正常版本的add
. DEFINE_INLINE_ADD()
将允许您访问一个名为 的函数inline_add
,该函数与您的普通函数具有相同的签名add
,但它具有不同的名称(inline_前缀)。
注意:使用标志__attribute((always_inline))__
时我没有使用-完成了这项工作。但是,如果您不想使用,请使用:-O3
__inline__
-O3
#define DEFINE_INLINE_ADD() GENERATE_ADD(static __inline__ __attribute__((always_inline)), inline_)
#include "add.h"
DEFINE_ADD()
DEFINE_ADD()
对宏生成器的简单调用。这将声明函数的正常版本(不会被内联的那个)。
#include <stdio.h>
#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
函数都将被内联。
#include <stdio.h>
#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
其他文件的正常版本。
#include <stdio.h>
#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 disass)。
/* prototypes for main */
int loop (void);
int loop2 (void);
int loop3 (void);
#include <stdio.h>
#include <stdlib.h>
#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;
}
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 <loop+16>
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 <loop2+51>
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 <add>
0x0000000000400611 <+31>:mov %eax,%esi
0x0000000000400613 <+33>:mov $0x400794,%edi
0x0000000000400618 <+38>:mov $0x0,%eax
0x000000000040061d <+43>:callq 0x400470 <printf@plt>
0x0000000000400622 <+48>:add $0x1,%ebx
0x0000000000400625 <+51>:cmp $0x1869f,%ebx
0x000000000040062b <+57>:jle 0x400602 <loop2+16>
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 <add>
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 <add>
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 <loop3+80>
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 个小时的敲击键盘试图弄明白,这是我能想到的最好的。请随时指出任何错误,我将非常感激。我对这个特殊的inline one function call非常感兴趣。
这里有个建议,把代码主体写在单独的头文件中。将头文件包含在它必须内联的位置,并包含在 C 文件的正文中以供其他调用。
void demo(void)
{
#include myBody.h
}
importantloop
{
// code
#include myBody.h
// code
}
我假设你的函数是一个小函数,因为你想内联它,如果是这样,你为什么不把它写在 asm 中呢?
至于仅内联对函数的特定调用,我认为没有什么可以为您完成这项任务。一旦一个函数被声明为内联,并且如果编译器会为你内联它,它会在它看到对该函数的调用的任何地方执行它。