C 效能榨汁機之虛函式的開銷

2022-06-19 22:36:13 字數 1858 閱讀 9689

**  

雖然c++標準並沒有規定編譯器實現虛函式的方式,但是大部分編譯器均是採用了虛函式表來實現虛函式,即對於每乙個包含虛成員函式的類生成乙個虛函式表,乙個指向虛函式表的指標被放在物件的首位址(不考慮多繼承等複雜情況),虛函式表中儲存該類所有的虛函式位址。當使用引用或者指標呼叫虛函式時,首先通過虛函式表指標找到虛函式表,然後通過偏移量找到虛函式位址並呼叫。關於虛函式表的更多細節,建議閱讀《深度探索c++物件模型》這本書。

空間開銷

首先,由於需要為每乙個包含虛函式的類生成乙個虛函式表,所以程式的二進位制檔案大小會相應的增大;其次,對於包含虛函式的類的例項來說,每個例項都包含乙個虛函式表指標用於指向對應的虛函式表,所以每個例項的空間占用都增加乙個指標大小(32位系統4位元組,64位系統8位元組)。這些空間開銷可能會造成快取的不友好,在一定程度上影響程式效能。

時間開銷

虛函式的時間開銷主要是增加了一次記憶體定址,通過虛函式表指標找到虛函式表,雖對程式效能有一些影響,但是影響並不大。

上述虛函式表面上的開銷其實是微不足道的,真正影響虛函式效能的是隱藏在背後的,不被人輕易察覺的,只有對計算機體系結構有一定理解才能探尋出藏在背後的「效能殺手」。

首先我們先看呼叫虛函式時,在彙編層生成了什麼**:

1

...

movq (%rax), %rax

movq (%rax), %rax

movq -24(%rbp), %rdx

movq %rdx, %rdi

call *%rax

...

上述彙編**最重要的就是第6行,在at&t格式彙編中,這是乙個間接呼叫,意義是從%rax指明的位址處讀取跳轉的目標位置。這也是虛函式呼叫與普通成員函式的區別所在,普通函式呼叫是乙個直接呼叫。直接呼叫與間接呼叫的區別就是跳轉位址是否確定,直接呼叫的跳轉位址是編譯器確定的,而間接呼叫是執行到該指令時從暫存器中取出位址然後跳轉。

有了上面的基本認識,我們就可以分析虛函式的效能開銷所在了,其實說到底,這個隱藏在背後的關鍵點就是分支**器,如果看過我之前的部落格,相信對分支**器已經很熟悉了,如果感覺分支**器還是很陌生,推薦閱讀我以前的分支**器的四篇文章:

c++效能榨汁機之分支**器1

c++效能榨汁機之分支**器2

c++效能榨汁機之分支**器3

c++效能榨汁機之分支**器4

有了分支**器和cpu指令流水線的基本知識,我們可以發現對於直接呼叫而言,是不存在分支跳轉的,因為跳轉位址是編譯器確定的,cpu直接去跳轉位址取後面的指令即可,不存在分支**,這樣可以保證cpu流水線不被打斷。而對於間接定址,由於跳轉位址不確定,所以此處會有多個分支可能,這個時候需要分支**器進行**,如果分支**失敗,則會導致流水線沖刷,重新進行取指、解碼等操作,對程式效能有很大的影響。

網上有部分文章中說對於虛函式這種間接跳轉會直接導致流水線沖刷,這種說法明顯是自相矛盾的,如果間接跳轉必定會導致流水線沖刷,那把這些指令放進流水線的意義何在呢?其實查閱資料就可以知道,intel和amd的cpu中存在兩級自適應**器用於**間接跳轉,此**器可以**多分支跳轉。

本文**出影響到虛函式呼叫效能的背後原因是流水線和分支**,由於虛函式呼叫需要間接跳轉,所以會導致虛函式呼叫比普通函式呼叫多了分支**的過程,產生效能差距的原因主要是分支**失敗導致的流水線沖刷效能開銷。

本文的目的並不是為了說明虛函式呼叫有額外開銷而讓大家避免使用虛函式,使用不使用虛函式應該由自己程式的需要而定,如果程式邏輯需要使用動態繫結,如果不使用虛函式而是自己實現相應邏輯的話產生的效能損耗一般會比使用虛函式的效能損耗大得多。但對於一些效能敏感的程式,在虛函式可用可不用的時候,可以考慮不使用虛函式以提高效能。

***************=== end

c 動態繫結的解析及虛函式帶來的開銷

每個支援虛函式的類 基類或派生類 都會有乙個其所有支援的虛函式指標的虛函式表,每個該類生成的物件都會隱含乙個虛函式指標,此指標指向其所屬類的虛函式表,當通過基類的指標或引用呼叫每個虛函式時,系統首先定位這個指標或引用真正對應的物件所隱含的虛函式指標,然後虛函式指標會根據這個虛函式的名稱,對這個虛函式...

C 之 虛函式與純虛函式的用法

虛函式與純虛函式的相同點 1.都可以在子類中過載,以多型的形式被呼叫。2.通常都存在於抽象基類中,被繼承的子類過載,目的是提供乙個統一的介面。3.都不能使用static標示符 原因 虛函式是動態繫結的 虛函式與純虛函式的不同點 1.虛函式為了過載和多型的需要,已在類中被定義 即便定義為空 所以在子類...

反彙編之C 的虛函式

先來檢視一簡單例子 includeusing namespace std class base class derive public base int main 用vs2015除錯一番.檢視pb儲存子物件的位址.以上pb儲存了子類物件的位址,位址為0x003bfb64.來看看.這個子類物件的首4個...