はじめに
C++には菱形継承への解決方法として仮想継承というものがある
これまでの人生で使う場面がなかったので知らなかったのだが、どうもこいつは遅いみたいだ
要因としては、vtableが複雑になってオーバーヘッドが増すからっぽい
ここでは、深く調べるつもりはなかったのでアセンブリを読んだりはしてないが、
参考程度に、いくつかのパターンでとったベンチマークを紹介する
ベンチマーク
使用したサイトは
https://quick-bench.com
最適化はO2を使った
計測には以下のようなclassを使用した
struct Super{virtual ~Super()=default;};
struct Sub : Super{};
struct Sub2 : Super{};
struct VSub : virtual Super{};
struct VSub2 : virtual Super{};
今回は上記のように、継承したか、仮想継承したかで比較しているだけなので
菱形継承とか複雑化していくにつれて、結果が大きく変わってくることはあるかもしれない。
生成 ~ 破棄
以下のように生成し、基底クラスにアップキャストして破棄されるだけのコードで計測してみた
Super* ptr = new Sub{};
delete ptr;
------------- vs --------------
Super* ptr = new VSub{};
delete ptr;
これはあまり変わらなかった。
vtableが複雑になるぶん、初期化コストがかかったりしないのかな?
一応確認してみて、仮想継承してるほうが遅そうなものの
僅かな差すぎてベンチマークでは大きな差がでてこなかったっぽそう
dynamic_cast
いろいろな組あわせでdynamic_castによる型チェックの計測をしてみた
こちらも基底クラスにアップキャストしたものから行う
Super* ptr = new Sub{};
bool result = dynamic_cast<Sub*>(ptr) != nullptr;
------------- vs --------------
Super* ptr = new VSub{};
bool result = dynamic_cast<VSub*>(ptr) != nullptr;
https://quick-bench.com/q/lnApnNVNEYX_VFo0iDOH7lPMdpQ
仮想継承した派生クラスから他の派生クラスへdynamic_castする際は重たそうだ
仮想関数呼び出し
仮想関数を追加して速度比較
Super* ptr = new Sub{};
ptr->func();
------------- vs --------------
Super* ptr = new VSub{};
ptr->func();
これはあまり変わらない。
vtableの仕組み的には自然に思う。
typeid
いろいろな組あわせでtypeidによる型チェックの計測をしてみた
こちらも基底クラスにアップキャストしたものから行う
Super* ptr = new Sub{};
bool result = typeid(*ptr) == typeid(Sub);
------------- vs --------------
Super* ptr = new VSub{};
bool result = typeid(*ptr) == typeid(VSub);
これも大きな違いはなさそうだった
アップキャスト
今度は関数引数に渡す際のアップキャストで計測してみた
bool Func(Super*);
Sub* ptr = new Sub{};
bool result = Func(ptr);
------------- vs --------------
VSub* ptr = new VSub{};
bool result = Func(ptr);
これもそんな大きな差はなさそう
まとめ
- 仮想継承してるほうが、わずかに遅そう
- dynamic_castを使う際は速度に大きな差が見えたが、それ以外のところでは大きな差はでなかった
- ひし形継承など複雑になれば、大きなボトルネックになってくる可能性はあるのかも?
$\huge{仮想継承も菱形継承もしないに越したことはない}$