32

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

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

4

8 回答 8

14

在 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 */  }
于 2013-01-28T21:34:10.300 回答
12

可以启用每个翻译单元的内联(但不是每个调用)。虽然这不是问题的答案并且是一个丑陋的技巧,但它符合 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 中使用

于 2015-09-17T17:04:53.723 回答
6

在 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;
}

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

于 2013-01-28T23:04:15.343 回答
4

答案是它取决于您的功能、您的要求以及您的功能的性质。你最好的选择是:

  • 告诉编译器你想要它内联
  • 使函数静态(注意 extern,因为它的语义在某些模式下在 gcc 中会发生一些变化)
  • 设置编译器选项以通知优化器您想要内联,并适当地设置内联限制
  • 在编译器上打开任何无法内联的警告
  • 验证函数是内联的输出(您可以检查生成的汇编程序)。

编译器提示

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

使函数成为内联函数意味着对函数的调用尽可能快。此类建议的有效程度由实施定义

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

  • GNU 的__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)). 但为什么会这样呢?

其他形式的优化

您还可以考虑如何重组循环并避免分支。分支预测可以产生巨大的影响。有关这方面的有趣讨论,请参见:为什么处理排序数组比处理未排序数组更快?

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

于 2015-09-17T10:59:20.857 回答
3

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

内联.h

#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;
}

内联.c

#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 条指令之后发生。

于 2015-09-21T19:31:01.897 回答
3

有一个内核源代码#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

添加.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);

这看起来不太好,但减少了维护两个不同功能的工作。该函数在宏中完全定义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_)

添加.c

#include "add.h"

DEFINE_ADD()

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

循环.c

#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函数都将被内联。

循环2.c

#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其他文件的正常版本。

循环3.c

#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)。

循环.h

/* 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非常感兴趣。

于 2015-09-18T17:46:11.303 回答
1

这里有个建议,把代码主体写在单独的头文件中。将头文件包含在它必须内联的位置,并包含在 C 文件的正文中以供其他调用。

void demo(void)
{
#include myBody.h
}

importantloop
{
    // code
#include myBody.h
    // code
}
于 2015-09-18T15:48:40.283 回答
-2

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

至于仅内联对函数的特定调用,我认为没有什么可以为您完成这项任务。一旦一个函数被声明为内联,并且如果编译器会为你内联它,它会在它看到对该函数的调用的任何地方执行它。

于 2015-09-17T16:27:38.647 回答