對於C++開發者來說,inline是個再熟悉不過的關鍵字,因為默認的成員函數都是inline,也是常規高校教材中宣揚C++的“優勢”之一。
但是C語言其實也是支持inline關鍵字的,而且是很早期的gcc就支持了該關鍵字。在Linux0.12版本內核代碼中也用到了該關鍵字。
今天碼哥淺談一下這個關鍵字的作用和使用。
inline的作用淺顯一點說,就是將聲明瞭該關鍵字的函數不以call指令調用的方式來調用,而是直接將其展開在調用函數中。似乎感覺有點像宏展開的樣子?
實際不然,我們以一個C語言示例來進行說明:
<code>inline int foo(int a)
{
a *= 3;
return a;
}
int main(void)
{
int a = foo(2);
a += foo(1);
return a;
}/<code>
例子很簡單,foo函數被聲明瞭inline,作用是將輸入參數擴大三倍返回。
下面我們來看看這個例子的彙編是如何的。在此之前,需要重點提示:
inline關鍵字只有在開啟了編譯優化後才會啟用,且如果函數定義時不聲明為inline,那麼inline只發生在有inline聲明之後的調用點。
無編譯優化彙編
<code>$ gcc -S a.c/<code>
我們並未開啟任何編譯優化,其彙編如下:
<code>\t.file\t"c.c"
\t.text
\t.globl\tfoo
\t.type\tfoo, @function
foo:
.LFB0:
\t.cfi_startproc
\tpushq\t%rbp
\t.cfi_def_cfa_offset 16
\t.cfi_offset 6, -16
\tmovq\t%rsp, %rbp
\t.cfi_def_cfa_register 6
\tmovl\t%edi, -4(%rbp)
\tmovl\t-4(%rbp), %edx
\tmovl\t%edx, %eax
\taddl\t%eax, %eax
\taddl\t%edx, %eax
\tmovl\t%eax, -4(%rbp)
\tmovl\t-4(%rbp), %eax
\tpopq\t%rbp
\t.cfi_def_cfa 7, 8
\tret
\t.cfi_endproc
.LFE0:
\t.size\tfoo, .-foo
\t.globl\tmain
\t.type\tmain, @function
main:
.LFB1:
\t.cfi_startproc
\tpushq\t%rbp
\t.cfi_def_cfa_offset 16
\t.cfi_offset 6, -16
\tmovq\t%rsp, %rbp
\t.cfi_def_cfa_register 6
\tsubq\t$16, %rsp
\tmovl\t$2, %edi
\tcall\tfoo
\tmovl\t%eax, -4(%rbp)
\tmovl\t$1, %edi
\tcall\tfoo
\taddl\t%eax, -4(%rbp)
\tmovl\t-4(%rbp), %eax
\tleave
\t.cfi_def_cfa 7, 8
\tret
\t.cfi_endproc
.LFE1:
\t.size\tmain, .-main
\t.ident\t"GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
\t.section\t.note.GNU-stack,"",@progbits/<code>
可以看到在main中存在兩個call指令調用foo函數,我們的inline關鍵字作用並未生效。
編譯優化彙編
<code>$ gcc -S a.c -O/<code>
我們僅啟用O1優化,那麼看下彙編成了什麼樣子呢?
<code>\t.file\t"c.c"
\t.text
\t.globl\tfoo
\t.type\tfoo, @function
foo:
.LFB0:
\t.cfi_startproc
\tleal\t(%rdi,%rdi,2), %eax
\tret
\t.cfi_endproc
.LFE0:
\t.size\tfoo, .-foo
\t.globl\tmain
\t.type\tmain, @function
main:
.LFB1:
\t.cfi_startproc
\tmovl\t$9, %eax
\tret
\t.cfi_endproc
.LFE1:
\t.size\tmain, .-main
\t.ident\t"GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
\t.section\t.note.GNU-stack,"",@progbits/<code>
這裡,碼哥沒有做任何刪減。
讀者可能會發現,main中返回值直接給了一個立即數9,而不是jmp到foo或者使用其相關指令計算的。這說明了什麼呢?
這說明了,inline並不簡簡單單的類似宏擴展,而是編譯器在編譯時將foo代碼展開進main中,並且在優化剪枝等優化步驟中對展開後的整體內容做優化,進而發現foo的輸入與輸出以及foo的兩次調用結果是一個可推算的常數值。
因此,inline的作用不僅僅是避免了call指令的使用以及其關聯的壓棧彈棧等操作,更是可以讓編譯器對整體性能做出非常多改進優化,大幅提升性能。
但是,inline也不可濫用,這是因為原本只需要一份的函數被展開到整個工程中各個使用點上,雖然效率會有些許提升,但是指令數量可能會大幅增長,導致可執行程序體積過大。
閱讀更多 碼哥比特 的文章