既然編譯器可以判斷一個函式是否適合 inline,那還有必要自己加 inline 關鍵字嗎?

Gibson314 發表於 2022-11-24
作者:雷鵬
連結:
來源:知乎
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

編譯器的 inline 是為了讓程式設計師與編譯器互相配合,改善效能的機制,編譯器確實可以判斷函式是否適合 inline,最簡單的例如 getter/setter。然而,inline 是否比不 inline 更好,這中間是有模糊地帶的,這就需要一些規則和閾值。對於現代的編譯器,我們主動加了 inline,編譯器大概只會更改那些 inline 規則中的相應閾值。

然而,實際上在很多時候,我們可以精心地編碼,讓編譯器能更好地進行 inline,讓 inline 的效果更好。我這裡有一個典型的範例:


的 中 Client-Server 互動,使用了 中的序列化框架,該序列化框架初版完成於 2006 年,後來命名為 febird 庫在 google code 上開源,再後來 google code 停止服務,febird 遷移到 github,有段時間重新命名為 nark,之後重新命名為 ,目前 topling-zip 中程式碼的 namespace 仍是 terark。從 2006 年至今,除 namespace 名稱之外,該序列化框架的介面一直保持穩定,2016 年的時候,針對 C++11 進行了模板推導相關的大幅最佳化,但仍保持了介面的穩定。以下為原文正文,排版有輕微改動。

原文:
作者:         發表日期: 2009年04月04日
分類:    評論: 條     閱讀次數: 2,111 次

最佳化技術主要有:

(一)最佳化的 inline

1.1  頻繁呼叫的函式都使用 inline

但是值得注意的是,在 inline 的時候,只 inline 最頻繁的分支,很少走到的分支使用非 inline 函式,例如:

void InputBuffer::ensureRead(void* vbuf, size_t length) {
    // 為了效率,這麼實現可以讓編譯器更好地inline這個函式    // inline 後的函式體並儘可能小    if (m_cur+length <= m_end) {
        memcpy(vbuf, m_cur, length);
        m_cur += length;
    } else
        fill_and_ensureRead(vbuf, length);}

一般情況下,如果 length 是個不大的常數值,編譯器會把 memcpy 最佳化成賦值語句。至少在 VC2008 中我觀察到了這個最佳化。

但是這裡仍有一種不太最佳化的情況,在理想的情況下,編譯器應該把 m_cur/m_end 都放在暫存器中,只有在暫存器 Spill Out 的時候,才把它們的值從暫存器拷到物件,並呼叫 fill_and_ensureRead。但實際上編譯器沒有這麼做,每次都存記憶體讀取m_cur/m_end。這可能是編譯器觀察到 InputBuffer有點大,並且有 。

1.2  MinMemIO/MemIO/AutoGrowMemIO

這個幾個效率更高,但只能在記憶體中操作,編譯器的極端最佳化,在這裡得到了體現:在 中,編譯器沒有做到我想要的最佳化,但是在這裡,編譯器做到了,他吧 MinMemIO 放到了暫存器中。

(二)拋棄標準 C++ stream,使用簡單、直接的 Stream/Buffer

2.1  可以對各種流進行快速緩衝的StreamBuffer,包括

i.  效率高、最常用的:InputBuffer/OutputBuffer

ii.  效率高、不常用的:SeekableInputBuffer/SeekableOutputBuffer

iii.  效率稍差、不常用的:SeekableBuffer,可讀也可寫,共享一個位置指標

iv.  這幾個Buffer結構簡單,操作直接,結合編譯器inline可以達到很高的效率,同時可以和實際Stream互操作。

(三)使用 typetraits 識別可以 memcpy 的類,進一步最佳化

a)  基本型別都可以進行 memcpy,並且這個 memcpy 實際上被最佳化成了賦值

b)  對稍微複雜的型別,有兩種方法:

i.  直接dump,不管它的格式

實現簡單,只管dump就行,boost::archive::binary_xxx實現了這種最佳化,但是它只能對基本型別和使用者宣告為可直接dump的類最佳化。並且如果febird也使用這種最佳化,將不能對Portable格式最佳化。

ii.  直接dump,再轉化格式

就比較複雜,需要一些技巧,febird做到了一點,不管對Native還是Portable格式,都做到了最佳化。因為序列化使用宏來進行宣告,因此,應用程式碼不用改變,只要認真最佳化這個宏,就可以做到。febird使用了這樣的技巧:

DATA_IO_LOAD_SAVE(MyData1, &a&b&c&d&e&f&g&h)

在這個宏呼叫中第二個引數 &a&b&c&d&e&f&g&h 被使用了多次,其中有一次展開後將是是這樣的:

DataIO_load_vector_impl(dio, *this,
  DataIO_is_realdump<DataIO,0,true>()&a&b&c&d&e&f&g&h, 
  bswap)

其中高亮部分 DataIO_is_realdump<DataIO,0,true>()&a&b&c&d&e&f&g&h 將推匯出一個類 DataIO_is_realdump<DataIO, Size, IsDumpable>,其中 Size 是 abcdefgh 的尺寸之和,IsDumpable 是abcdefgh 的 IsDumpable 的 and 結果,DataIO_load_vector_impl 以這個類為引數,進行函式呼叫的自動分派,如果 Size==sizeof(MyData1) 就說明 MyData 中沒有編譯器為對齊成員自動產生的 Padding,如果IsDumpable 同時為 true,那麼這個類就可以被 dump。但是這裡仍然有一個潛在的危險:

如果 &a&b&c&d&e&f&g&h 的順序和它們在 類定義中出現的順序不同,並且這個不同是有意為之,而不是粗心大意(雖然這個可能性很低,但不代表不存在),那麼這個最佳化產生的行為將 違背呼叫者的真實意圖。關於這一點,無法進行自動檢查,因此使用者需要 特別注意。如果 要測試是否出現了這種錯誤,可以先禁用這種最佳化,產生資料,然後使用最佳化,來讀取資料,如果資料格式不同,就說明出了錯。

(四)效果

使用了這麼多最佳化, ,平均情況下,如果是基本型別 vector,比 boost 快不了太多,但是對複雜型別,比 boost 要 快20~50倍,如果資料已經過驗證,不用擔心越界,讀取時可以使用NativeDataInput<MinMemIO>,此時速度更加驚人: 比 boost 快 1600 倍!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70013015/viewspace-2924946/,如需轉載,請註明出處,否則將追究法律責任。