[英](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;
}
另請注意,強制內聯函數可能不會使您的代碼更快。 內聯函數會導致生成更大的代碼,這可能會導致發生更多的緩存未命中。 我希望這有幫助。
答案是這取決於您的功能、您的要求以及您的功能的性質。 你最好的辦法是:
編譯器提示
這里的答案僅涵蓋內聯的一方面,該語言向編譯器提示。 當標准說:
使函數成為內聯函數意味着對該函數的調用盡可能快。 這些建議的有效程度是由實施定義的
對於其他更強大的提示,情況可能就是這種情況,例如:
__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
#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_)
#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 個小時的敲擊鍵盤試圖弄清楚,這是我能想到的最好的方法。 請隨時指出任何錯誤,我將不勝感激。 我得到了真正的興趣在這個特殊的內聯一個函數調用。
如果您不介意為同一個函數使用兩個名稱,則可以在函數周圍創建一個小包裝器,以“阻止” always_inline 屬性影響每次調用。 在我的示例中, loop_inlined
將是您在性能關鍵部分使用的名稱,而普通loop
將用於其他任何地方。
#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.