RTTI 虛函式和虛基類的開銷分析及使用指導

2021-08-17 05:11:31 字數 3336 閱讀 3050

「在正確的場合使用恰當的特性」 對稱職的c++程式設計師來說是乙個基本標準。想要做到這點,首先要了解語言中每個特性的實現方式及其開銷。本文主要討論相對於傳統c而言,對效率有影響的幾個c++新特性。

c++引入的額外開銷體現在以下兩方面:

模板、類層次結構、強型別檢查等新特性,以及大量使用了這些新特性的c++模板、演算法庫都增加了c++編譯器的負擔。但是應當看到,這些新機能在不降低,甚至(由於模板的內聯 能力)提公升了程式執行效率的前提下,明顯減輕了廣大c++程式設計師的編碼量。用幾秒鐘的cpu時間換取幾人日的辛勤勞動,附帶節省了日後debug和維護**的時間,這點開銷當算超值。

當然,在使用這些特性的時候,也有不少優化技巧。比如:編譯乙個大型軟體時,幾條顯式例項化指令就可能使編譯速度提高幾十倍;恰當地組合使用部分專門化和完全專門化,不但可以最優化程式的執行效率,還可以讓同時使用多種不同引數例項化模板的軟體體積顯著減小……

執行時開銷恐怕是程式設計師最關心的問題之一了。相對與傳統c程式而言,c++中有可能引入額外執行時開銷的新特性包括:

虛基類虛函式rtti(dynamic_cast和typeid)

異常物件的構造和析構

關於其中第四點:異常,對於大多數現代編譯器來說,在正常情況(未丟擲異常)下,try塊中的**執行效率和普通**一樣高,而且由於不再需要使用傳統上通過返回值或函式呼叫來判斷錯誤的方式,**的實際執行效率還可能進一步提高。丟擲和捕捉異常的效率也只是在某些情況下才會稍低於函式正常返回的效率,何況對於乙個編寫良好的程式,丟擲和捕捉異常的機會應該不多。關於異常使用的詳細討論,參見:c++編碼規範正文。

而第五點物件的構造和析構開銷也不總是存在。對於不需要初始化/銷毀的型別,並沒有構造和析構的開銷,相反對於那些需要初始化/銷毀的型別來說,即使用傳統的c方式實現,也至少需要與之相當的開銷。 這裡要注意的一點是盡量不要讓構造和析構函式過於臃腫,特別是在乙個類層次結構中更要注意。時刻保持你的構造、析構函式中只有最必要的初始化和銷毀操作,把那些並不是每個(子)物件都需要執行的操作留給其他方法。

其實對乙個優秀的編譯器而言,c++的各種特性本身就是使用c/彙編加以千錘百鍊而最優化實現的。可以說,想用c甚至彙編比編譯器更高效地實現某個c++特性幾乎是不可能的。要是真能做到這一點的話,大俠就應該去寫個編譯器造福廣大程式設計師才對~

c++之所以比c「低效」,其根本原因在於:由於對某些特性的實現方式及其 導致的開銷不夠了解,致使程式設計師在錯誤的場合使用了錯誤的特性。而這些錯誤基本都集中在:

其中 前兩點上文已經講過,下面討論第三點。

為了說明rtti、虛函式和虛基類的實現方式,首先給出乙個例項及其具體實現(為了便於理解,這裡故意忽略了一些無關緊要的優化):

圖中虛箭頭代表偏移,實箭頭代表指標

由上圖得到每種特性的執行時開銷如下:

特性時間開銷

空間開銷

rtti

幾次整形比較和一次取址操作(可能還會有1、2次整形加法)

每型別乙個type_info物件(包括型別id和類名稱),典型情況下小於32位元組

虛函式一次整形加法和一次指標間接引用

每型別乙個虛表,典型情況下小於128位元組每物件若干個(大部分情況下是乙個)虛表指標,典型情況下小於8位元組

虛基類從直接虛繼承的子類(例如上圖中的 "b1" 和 "b2",但不包括 "dd" )中訪問虛基類的資料成員或其虛函式時,將增加兩次指標間接引用和一次整形加法(大部分情況下可以優化為一次指標間接引用)。

每型別乙個虛基類表,典型情況下小於32位元組每物件若干虛基類表指標,典型情況下小於8位元組

在同時使用了虛函式的時候,虛基類表可以合併到虛表(virtual table)中,每物件的虛基類表指標(vbptr)也可以省略(只需vptr即可)。

* 其中「每型別」或「每物件」是指用到該特性的型別/物件。對於未用到這些功能的型別及其物件,則不會增加上述開銷

可見,關於老天「餓時掉餡餅、睡時掉老婆」等美好傳說純屬謠言。但凡人工製品必不完美,總有設計上的取捨,有其適應的場合也有其不適用的地方。

c++中的每個特性,都是從程式設計師平時的生產生活中逐漸精化而來的。在不正確的場合使用它們必然會引起邏輯、行為和效能上的問題。對於上述特性,應該只在必要、合理的前提下才使用。

"dynamic_cast" 用於在類層次結構中漫遊,對指標或引用進行自由的向上、向下或交叉強制。"typeid" 則用於獲取乙個物件或引用的確切型別,與 "dynamic_cast" 不同,將 "typeid" 作用於指標通常是乙個錯誤, 要得到乙個指標指向之物件的type_info,應當先將其解引用(例如:"typeid(*p);")。

一般地講,能用虛函式解決的問題就不要用 "dynamic_cast",能夠用 "dynamic_cast" 解決的就不要用 "typeid"。比如:

void

rotate(

inconst

cshape

& is

)elseif(

typeid(is

) == 

typeid

(c********

))elseif(

typeid(is

) == 

typeid

(csqucre

))// ...

}以上**用 "dynamic_cast" 寫會稍好一點,當然最好的方式還是在cshape裡定義名為 "rotate" 的虛函式。

虛函式是c++執行時多型特性中開銷最小,也最常用的機制。虛函式的好處和作用這裡不再多說,應當注意在對效能有苛刻要求的場合,或者需要頻繁呼叫,對效能影響較大的地方(比如每秒鐘要呼叫成千上萬次,而自身內容又很簡單的事件處理函式)要慎用虛函式。

需要特別說明的一點是:虛函式的呼叫開銷與通過函式指標的間接函式呼叫(例如:經典c程式中常見的,通過指向結構中的乙個函式指標成員呼叫;以及呼叫dll/so中的函式等常見情況)是相當的。比起函式呼叫本身的開銷(儲存現場->傳遞引數->傳遞返回值->恢復現場)來說,一次指標間接引用是微不足道的。這就使得在絕大部分可以使用函式的場合中都能夠負擔得起虛方法的些微額外開銷。

作為一種支援多繼承的物件導向語言,虛基類有時是保證類層次結構正確一致的一種必不可少的手段。在需要頻繁使用基類提供的服務,又對效能要求較高的場合,應該盡量避免使用它。

在基類中沒有資料成員的場合,也可以解除使用虛基類。例如,在上圖中,如果類 "bb" 中不存在資料成員,那麼 "bb" 就可以作為乙個普通基類分別被 "b1" 和 "b2" 繼承。這樣的優化在達到相同效果的前提下,解除了虛基類引起的開銷。不過這種優化也會帶來一些問題:從 "dd" 向上強制到 "bb" 時會引起歧義 ,破壞了類層次結構的邏輯關係。

上述特性的空間開銷一般都是可以接受的,當然也存在一些特例,比如:在儲存布局需要和傳統c結構相容的場合、在考慮對齊的場合、在需要為乙個本來 尺寸很小的類同時例項化許多物件的場合等等。

RTTI 虛函式和虛基類的開銷分析及使用指導

在正確的場合使用恰當的特性 對稱職的c 程式設計師來說是乙個基本標準。想要做到這點,首先要了解語言中每個特性的實現方式及其開銷。本文主要討論相對於傳統 c 而言,對效率有影響的幾個c 新特性。相對於傳統的 c 語言,c 引入的額外開銷體現在以下兩個方面 模板 類層次結構 強型別檢查等新特性,以及大量...

RTTI 虛函式和虛基類的開銷分析及使用指導

在正確的場合使用恰當的特性 對稱職的c 程式設計師來說是乙個基本標準。想要做到這點,首先要了解語言中每個特性的實現方式及其開銷。本文主要討論相對於傳統c而言,對效率有影響的幾個c 新特性。c 引入的額外開銷體現在以下兩方面 模板 類層次結構 強型別檢查等新特性,以及大量使用了這些新特性的c 模板 演...

虛基類 虛函式和純虛基類

首先看乙個例子 class base class child1 public base class child2 public base void main else p print 函式呼叫的時候,檢視虛表,根據p的位址首先從虛表裡面查詢要呼叫的函式 這裡呼叫child2的print 函式 ret...