簡體   English   中英

(如何)我可以內聯特定的函數調用嗎?

[英](How) Can I inline a particular function call?

假設我有一個在程序的多個部分中被調用的函數。 假設我有一個對該函數的特定調用,該調用位於對性能極其敏感的代碼部分(例如,一個循環迭代數千萬次並且每一微秒都很重要)。 有沒有辦法可以強制編譯器(在我的例子中為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 <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!

您還可以在 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;
}

另請注意,強制內聯函數可能不會使您的代碼更快。 內聯函數會導致生成更大的代碼,這可能會導致發生更多的緩存未命中。 我希望這有幫助。

答案是這取決於您的功能、您的要求以及您的功能的性質。 你最好的辦法是:

  • 告訴編譯器你想要它內聯
  • 使函數保持靜態(注意 extern,因為它的語義在某些模式下在 gcc 中會發生一些變化)
  • 設置編譯器選項以通知優化器您想要內聯,並適當地設置內聯限制
  • 在編譯器上打開任何無法內聯警告
  • 驗證輸出(您可以檢查生成的匯編程序)該函數是內聯的。

編譯器提示

這里的答案僅涵蓋內聯的一方面,該語言向編譯器提示。 當標准說:

使函數成為內聯函數意味着對該函數的調用盡可能快。 這些建議的有效程度是由實施定義的

對於其他更強大的提示,情況可能就是這種情況,例如:

  • GNU 的__attribute__((always_inline)) :通常,除非指定優化,否則函數不會內聯。 對於聲明為 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)) 但為什么會這樣呢?

其他形式的優化

您還可以考慮如何重構循環並避免分支。 分支預測可以產生巨大的影響。 有關對此的有趣討論,請參閱: 為什么處理已排序數組比處理未排序數組更快?

然后,您還可以展開較小的內部循環並查看不變量。

有一個內核源代碼,它以一種非常有趣的方式使用#define來定義具有相同 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()將被調用add.c產生的正常版本add DEFINE_INLINE_ADD()將允許您訪問名為inline_add的函數,該函數與您的普通add函數具有相同的簽名,但具有不同的名稱( inline_前綴)。

注意:在使用-O3標志時我沒有使用__attribute((always_inline))__ - __inline__完成了這項工作。 但是,如果您不想使用-O3 ,請使用:

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

添加.c

#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函數都將被內聯。

循環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 個小時的敲擊鍵盤試圖弄清楚,這是我能想到的最好的方法。 請隨時指出任何錯誤,我將不勝感激。 我得到了真正的興趣在這個特殊的內聯一個函數調用

如果您不介意為同一個函數使用兩個名稱,則可以在函數周圍創建一個小包裝器,以“阻止” always_inline 屬性影響每次調用。 在我的示例中, loop_inlined將是您在性能關鍵部分使用的名稱,而普通loop將用於其他任何地方。

內聯.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;
}

內聯文件

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

無論優化級別如何,這都有效。 在 Intel 上使用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 中編寫它?

至於僅內聯對函數的特定調用,我認為沒有什么可以為您完成此任務。 一旦一個函數被聲明為內聯,並且如果編譯器將為您內聯它,它將在任何看到對該函數的調用的地方執行它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM