現在的編譯器的inline策略是怎樣的?

以前聽說過很多類似的傳聞,比如C的inline就是裝飾品啥的,但不知道他們到底是以怎樣的策略在inline,很好奇QAQ


c/c++語言級別的inline關鍵字確實有點名不副實,編譯器是否對某個函數進行內聯基本上看的是優化級別,當然使用GCC擴展的no inline, always inline屬性能影響到計算被調用函數的內聯代價計算。而且如何選擇內聯策略在不同的編譯器之間存在較大差異,甚至同一個編譯器不同版本之間也不盡相同。比如在llvm編譯器中,4.0版本中會根據優化級別來,當優化級別&<= 0(也就是-O0, O1的時候)採用AlwaysInliner,更高的優化級別(-O2, -O3, -O4)則會採用SimpleInliner。

如下BackendUtil.cpp:314行的代碼。

而在LLVM 2.6中,當-O0的時候,不進行內聯。-O1的時候SimpleInliner,-O2, -O3的時候採用alwaysInliner,而且SimpleInliner器的代碼大小閾值還和優化級別相關。

你看,對比上面兩個版本之間的選擇,他們的內聯選擇策略甚至是相反的。

通常上述的選擇取決你是想犧牲空間換取運行時間,還是犧牲運行時間換取存儲空間大小。

選擇不同的內聯器之後,在進行內聯的時候,會考慮被內聯函數的linkage級別,比如static函數就會容易被內聯,僅僅被調用一次也會降低被內聯的代價,還有一些調用規範(如Cold)會增加內聯代價。當然的基本塊數目,IR指令數目,循環嵌套深度等都會影響到被調用函數的內聯代價。

知道被調用函數的內聯代價之後,就是遍歷SCC(CFG圖的強連通分量),依次判斷內聯代價是否小於上述中指定的閾值,成立則會進行內聯。內聯的處理方式就是使用實參替換callee中的形參,替換存儲返回值的變數為callee函數返回值變數。然後將callee內部的基本塊複製到caller中,然後替換所有的函數調用指令CallInst為無條件跳轉至callee的第一個基本塊,將callee的return指令替換為無條件跳轉至函數調用指令之後的位置。


總的來說,inline的策略怎麼樣是故意不披露的,這樣至少保留了每個版本策略發生變化的餘地。C的inline當然是有用的,但是編譯器自己會有一個判斷,根據你編譯器參數的選擇,看看到底這個地方是犧牲空間來inline好,還是犧牲inline來節省空間好。

程序太大也會導致exe自己保存在內存中的機器碼頻繁緩存失效降低性能的。如果你是使用VC++的話,你可以使用__forceinline。C++的成員函數默認是有inline的,只要實現寫在了類里。


離一下題,鑒於有人說 inline 是裝飾的情況。

C/C++ 的 inline 關鍵字和 inline 動作是不同的。

前者保證的是後者不發生時,仍然用同一份函數定義,換言之是為內聯失敗的情況擦屁股。

後者則是不管發生與否,一般在 As-if 規則下不可分辨。

強制內聯是編譯器擴展,例如 MSVC 的 __forceinline 和 GCC 的 __attribute__((always_inline)) 。

至於具體的內聯策略我不了解就不說了,可以認為上面這些完全離題。


主要策略就是衡量成本和收益。

首先,這個成本/收益的分析是基於每個調用的。也就是說,同一個被調函數,可能在一個地方被內聯了,而在另一個地方沒有。

舉個栗子:

void callee(int x) { ... }

void caller(int v) {

....

callee(10); //可能被內聯了

....

callee(v); //可能沒有被內聯

其次,「成本」主要是代碼大小的變化。收益主要是運行性能的提高。通常的概念是,空間換時間。但實際上,「成本」可以是負數,也就是說會減小代碼的大小。實際應用中,完全禁止內聯通常反而導致代碼更大。

比如說,一個靜態callee,只被調用一次,那麼inlining反而減少了調用/返回的代碼。再比如

void callee(void *ptr) { if (ptr) {....} else {...} };

void caller() { int x; callee(x);} // 編譯器是知道參數一定是非空的,所以內聯後, "else"那部分就是死代碼了。同理,還有常量參數的propagation. 在每個調用處,編譯器會分析,假如參數已知,被調函數會如何被簡化。

模型里還有一些"Bonus", 比如有「inline」關鍵字,就扣掉些成本。函數執行的次數多一些(比如在Inner Loop里或者有PGO信息), 也可以給寫Bonus。還有一下隱藏的命令行參數可以影響模型的估計。

具體到LLVM里,以當前版本6.0為例,主要代碼在lib/Analysis/InlineCost.cpp里

除了估計成本以外,Inline的順序也會導致結果的不同。比如A calls B, B calls C, 如果先把C inline 進 B,那麼可能A就不會再Inline B了,因為B變太大了。如果A先Inline B,那麼可能C也會被Inline進去。

總而言之,如果非要Inline一個函數, 那麼就用attribute((always_inline))。如果一定不要Inline,就用noinline。其他就留給編譯器去考慮吧。


可以看看下面的Chakra JIT裡面對JavaScript函數怎麼做的inline decision。一些經常被考慮的包括,被調函數的大小(太大的不會inline,太小的總會被inline),被調用次數的profile數據,被調函數里有沒有異常處理和循環等。

InliningDecider::DeciderInlineIntoInliner


編譯器內部會有一個閾值,在編譯一個函數的時候,會計算每條語句所貢獻的值(這個值編譯器內部有一個映射演算法),加完看是否超過,超過就不做inline,不超過就inline。當然這中間還有一些check看看是否能inline之類的。不同的優化選項這個值會不一樣,來tradeoff 性能和空間。這個是llvm的代碼,參數就是這個閾值,這個值是可以變化的。

Pass *llvm::createFunctionInliningPass(int Threshold) {

return new SimpleInliner(llvm::getInlineParams(Threshold));

}


其實這個東西有點沒設計好。

大部分情況下,編譯器判斷比人准。

少部分情況下,程序員做了剖分或者分析,認為確實該inline,所以建議未必沒用。

問題是,該不該inline,不應該是這麼不靈活的。要求應該可以,一個call要求inline,另一個不要求。


總之inline只是對編譯器的一個建議,建議編譯器在可能的情況下在調用點展開函數體,但是是否展開完全取決於編譯器自己. 另外即使沒有聲明成inline的函數也可能因為被優化而展開。有的編譯器甚至直接忽略inline關鍵字.

C++編譯器對inline的處理基本就是遵循上面的原則,但是遵循ISO或者GNU標準的C編譯器對inline的處理就略微詭異了.

用Clang嘗試編譯類似如下一段C代碼(不是C++). 當編譯優化設置為-O0時,鏈接器報告找不到符號foo,當優化設置為-O2時,卻能正常編譯.

inline int foo(int a, int b) { return a + b; }

int main(int argc, char *argv[])
{
return foo(1, 2);
}

這是為什麼?從C標準文檔ISO/IEC 9899可以找到答案.

1. ANSI C, ISO/IEC C89/C90:標準中沒有inline關鍵字.

2. GNU C89/C90:

(a) static inline:函數名標識符的作用域為當前編譯單元(translation unit),允許其他編譯單元中有重名定義. 這裡的inline建議編譯器,函數在被調用時可以直接展開函數體,但是否展開取決於編譯器.(譬如,如果優化級別為-O0,則必須按函數地址調用,此時編譯器會忽略inline請求,將函數編譯為普通函數;或者,出現了遞歸調用,編譯器也無法內聯這個函數)

(b) inline:在當前編譯單元內,和static inline語義相同,都是建議編譯器在當前編譯單元內展開函數體(是否展開取決於編譯器). 但同時編譯器會對該函數生成一份普通函數的代碼,在其他編譯單元內可以調用,與普通的extern函數調用無異.

(c) extern inline:相當詭異. 這樣的函數定義只為內聯而提供. 如果強行用普通函數調用方式調用該函數(譬如,優化級別為-O0,或者按函數指針調用),則鏈接器會認為存在另一個同名的普通函數. 如果沒有這個同名普通函數的定義,則鏈接器會報告找不到符號.

3. ISO/IEC C99/C11:

(a) static inline:和GNU C89/C90中的語義完全相同.

(b) inline:很類似GNU C89/C90中的extern inline. 標準文檔中的解釋相當晦澀:允許(但不要求)編譯器在當前編譯單元內展開函數體(原文的描述是「相比正常的函數調用機制,讓內聯函數調用儘可能快」,而文檔下面的腳註中提到,可能的選擇是「內聯替換」,見ISO/IEC 9899:1999或ISO/IEC 9899:2011),是否內聯由編譯器設計者自行決定,同時也允許外部存在同名的普通函數定義. 經測試最新版本的Clang和GCC在標準-std=c99和-std=c11下會在可以內聯的情況下(例如優化級別為-O2)採用內聯版本.

但標準文檔中同時也規定了,若在函數原型中加入extern,則相應的內聯函數定義成為所謂的「外部定義」,行為和GNU C89/C90的inline相同:在當前編譯單元中建議編譯器展開函數體,同時生成一份普通函數的代碼,在其他編譯單元中也可調用.

(c) extern inline:標準文檔中未見extern inline的定義.

4. GNU C99/C11:採用與ISO/IEC C99/C11相同的語義.

最新版本的Clang和GCC默認均採用GNU C11標準,因此會出現不優化代碼時找不到內聯函數符號的錯誤.


你覺得你沒有編譯器靠譜的時候用inline

你覺得編譯器沒你靠譜的時候用forceinline


大話CPU中說減少過程調用能提高效率,我理解的inline就是把函數調用擴展到代碼中,是為了提高速度。相近的技術是函數參數和值的緩存技術,也是犧牲空間換時間的做法。追求速度的可以用。


推薦閱讀:

devcpp編譯生成一個無許可權運行的exe,並且無法再次修改編譯也無法刪除exe,如何解決?
誰看完過龍書虎書鯨書?全部看完是不是就有能力寫一個C語言的編譯器了?
c++有哪些像__gcd這樣的編譯器自帶函數?
為什麼 VC 不允許 x64 內聯彙編?
什麼語言最適合寫編譯器/解釋器?

TAG:編譯原理 | GCC | 編譯器 | LLVM | 編譯器優化 |