Item 30: Understanding the ins and outs of lining.
inline(內聯)函數的好處太多了:它沒有宏的那些缺點,見Item 2:避免使用define;而且不需要付出函數調用的代價。 同時也方便了編譯器基於上下文的優化。但inline函數也並非免費的午餐:
它會使得目標代碼膨脹,運行時會占用更多的內存,甚至引起緩存頁的失效和指令緩存的Miss,這些都會造成運行時性能的下降。 但是另一方面,如果inline函數足夠小以至於生成的目標代碼比函數調用還小,那麼inline函數會產生更小的目標代碼以及更高的指令緩存命中率。
本文便來討論一些典型的適合inline的場景,以及容易誤用inline的地方。
可能很多開發者不知道,inline只是對編譯器的一個請求而非命令。該請求可以隱式地進行也可以顯式地聲明。
當你的函數較復雜(比如有循環、遞歸),或者是虛函數時,編譯器很可能會拒絕把它inline。因為虛函數調用只有運行時才能決定調用哪個,而inline是在編譯器便要嵌入函數體。 有些編譯器在dianotics級別編譯時,會對拒絕inline給出warning。
隱式的辦法便是把函數定義放在類的定義中:
class Person{
...
int age() const{ return _age;} // 這會生成一個inline函數!
};
例子中是成員函數,如果是友元函數也是一樣的。除非友元函數定義在類的外面。
顯式的聲明則是使用inline
限定符:
template
inline const T& max(const T& a, const T& b){ return a
可能你也注意到了inline函數和模板一般都定義在頭文件中。這是因為inline操作是在編譯時進行的,而模板的實例化也是編譯時進行的。 所以編譯器時便需要知道它們的定義。
在絕大多數C++環境中,inline都發生在編譯期。有些環境下也可以在鏈接時進行inline,尤其在.NET中可以運行時進行inline。
但模板實例化和inline是兩個過程,如果你的函數需要做成inline的就把它聲明為inline(也可以隱式地),否則仍然把它聲明為正常的函數。
有些適合inline的函數編譯器仍然不能把它inline,比如你要取一個函數的地址時:
inline void f(){} void (*pf)() = f; f(); // 這個調用將會被inline,它是個普通的函數調用 pf(); // 這個是通過指針調用的,不會被inline
構造析構函數看起來很適合inline,但事實並非如此。我們知道C++會在對象創建和銷毀時保證做很多事情,比如調用new
時會導致構造函數被調用, 退出作用域時析構函數被調用,構造函數調用前成員對象的構造函數被調用,構造失敗後成員對象被析構等等。
這些事情不是平白無故發生的,編譯器會生成一些代碼並在編譯時插入你的程序。比如編譯後一個類的構造過程可能是這樣的:
Derived::Derived(){ Base::Base(); try{ data1.std::string::string(); } catch(...){ Base::Base(); throw; } try{ data2.std::string::string(); } catch(...){ data1.std::string::~string(); Base::~Base(); throw; } ... }
Derived的析構函數、Base的構造和析構函數也是一樣的,事實上構造和析構函數會被大量地調用。 如果全部inline的話,這些調用都會被擴展為函數體,勢必會造成目標代碼膨脹。
如果你是庫的設計者,那麼你的接口函數的inline
特性的變化將會導致客戶代碼的重新編譯。 因為如果你的接口是inline的,那麼客戶需要將函數體展開編譯到客戶的目標代碼中。
那麼我們應當如何決定是否inline呢?最初我們不應inline任何東西,除非它是必須被inline的或者真的是很顯然(比如前述的age()
方法)。 況且只有20%的代碼會決定80%的性能,當我們遇到那20%性能關鍵的部分時再去inline它不遲!
inline函數也是不易調試的。。因為它被inline了。