仮想関数テーブル
出典: フリー百科事典『ウィキペディア(Wikipedia)』 (2023/11/25 17:30 UTC 版)
多重継承とthunk
g++ コンパイラはクラス D
の B1
と B2
からの多重継承を、基底クラスごとに一つずつの、二つの仮想関数テーブルを用いて実現する(多重継承を実現するには他にも方法があるがこれが最も一般的である)。これにより、キャストの際 "pointer fixups" (thunk) が必要になる。
下記のような C++ コードを考える:
D *d = new D();
B1 *b1 = dynamic_cast<B1*>(d);
B2 *b2 = dynamic_cast<B2*>(d);
d
と b1
が実行時に同じメモリ位置を参照するが、
b2
は d+8
(d
のメモリ配置の8バイト後方)を示す。
そのため、b2
は d
内の B2
らしく見える領域、
すなわち B2
のインスタンスと同じメモリレイアウトを持つ部分を示す。
呼び出し
呼び出しの際には、d->f1()
の呼び出しの際は、d
の D::B1
vpointer をたどり、vtable から f1
のエントリーを調べ、ポインタを取り出してコードを呼び出す。
単一継承(あるいは、単一継承のみ可能な言語)の場合、vpointer が常に d
の最初の要素にあれば(多数のコンパイラでそうなっている)、下記のような擬似 C++ のコードに簡略化できる。
*((*d)[0])(d)
より一般的なケースでは、上記のようなd
の f1()
、D::f2()
、B2::f2()
呼び出しはより複雑なものとなる。
*((d->/* Dの(B1用の)仮想関数テーブルへのポインタ*/)[0])(d)
*((d->/* Dの(B1用の)仮想関数テーブルへのポインタ*/)[12])(d)
*((d->/* Dの(B2用の)仮想関数テーブルへのポインタ*/)[0])(d+8)
これに対して、d->f0()
の呼び出しはもっと単純である:
*B1::f0(d)
効率
単なるコンパイルされたポインタへのジャンプである非仮想関数の呼び出しに対して、仮想関数の呼び出しは最低一度以上、余分にポインタをたどる操作や"fixup" が必要である。そのため、仮想関数の呼び出しは原理的に非仮想の関数呼び出しに対して低速である。実験によれば 6-13% の実行時間が単なる関数のディスパッチに用いられ、オーバーヘッドは場合によって 50% に達する[1]。
さらに、 JIT コンパイルが使用できない環境では、仮想関数は通常インライン展開できない。テーブルの参照を行う部分を、たとえばインライン化された本体部分を条件文で実行させることも可能ではあるが、そうした最適化は一般的ではない。
オーバーヘッドを避けるため、コンパイラはコンパイル時に呼び出しが解決できる場合には vtable の生成を行わない。
従って、上記の f1
の呼び出しは、 d
が現時点で D
のみ保持しており、D
が f1
をオーバーライドしないことをコンパイラが判断できるため、vtable の検索は必要なくなる可能性がある。コンパイラ(あるいは最適化プログラム)はプログラム内に f1
をオーバーライドする B1
のサブクラスがないことを検出することができるかもしれない。実装が明示的に指定されているため(this ポインタの fixup が必要ではあるが)B1::f1
または B2::f2
はおそらく vtable の検索を必要とすることはない。
注釈
出典
- ^ Driesen, Karel and Holzle, Urs, "The Direct Cost of Virtual Function Calls in C++", OOPSLA 1996
- ^ Zendra, Olivier and Driesen, Karel, Stress-testing Control Structures for Dynamic Dispatch in Java", Pp. 105?118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02)
- 1 仮想関数テーブルとは
- 2 仮想関数テーブルの概要
- 3 多重継承とthunk
- 4 比較、およびその他の方法
- 仮想関数テーブルのページへのリンク