Chapter 2 :C++に於ける仮想関数の実行コストに関する考察
§1 : 序
仮想関数は一般的に遅いと認識されているようである。
しかし、その実際はいかがなものであろうか。
§2 : それぞれの関数呼び出しの動作原理
仮想関数は仮想関数テーブルに呼び出すアドレスが格納されている。
クラスはインスタンスを生成すると、非スタティックメンバ変数及び、仮想関数テーブルへの
ポインタがメモリに確保される。
これによりインスタンスが基底クラスのポインタにキャストされても呼び出すべき
関数を正しく認識することができる。
一方普通のメンバ関数は、コンパイル時に呼び出すべき関数が特定できるため、
メモリの値を経由して呼び出すなどということは無い。
§3 : 各々の呼び出しのプロセス
VCでは、仮想関数テーブルへのポインタは確保されたメモリの先頭に置かれる。
ここで、
class a{
public:
virtual void foo();
virtual void bar();
}
のようなクラスがあったとすると、例えば、
_a->bar(); ( a *_a; として)
のようなコードは、VCでは
mov reg,[_a] ; 仮想関数テーブルのアドレス取得
mov ecx,_a ; thisポインタの用意
call [reg+4] ; 仮想関数テーブルにより関数呼び出し
(_aは実際にはレジスタまたはアドレス、regは何らかのレジスタ)
のようにコンパイルされる。
一方、これが仮想関数で無かったとすると、これは、
mov ecx,_a ; thisポインタの用意
call xxxxxxxx ; メンバ関数呼び出し
のようにコンパイルされます。
§4 : 実行コスト
さて、上記のコード、P6アーキテクチャで考えるなら、仮想関数のコードは2クロック、
関数呼び出しのコードは1クロックで完了してしまう。(いや…調べたわけじゃないんで…適当)
ただし、実際にはそんな短時間で処理が完了することはまず無い。
この辺にはキャッシュが関係してくる。
初めに、キャッシュが完全に効いた場合、
retするだけの関数の場合、うちのK6-IIIでは仮想関数だと11クロック、
普通のメンバ関数だと3クロックで呼び出し、リターンが完了する。
(何回やっても変わりなし)
キャッシュがうまく効かないように仕向けた場合、
仮想関数だと、10回試行して、704-1928クロック、平均1320.5クロック、
普通のメンバ関数で、条件同じく、213-915クロック、平均467.1クロック
この差は各呼び出しに於けるメモリ参照回数によるものだろう。
先のコードを参照して頂くとして、
メモリ参照回数は、仮想関数では2回、普通のメンバ関数は0回である。
これに加え、CALL命令によってジャンプ先アドレスのコードが参照されるので、
各1回参照回数は増え、結局は3回と1回になるものと思われる。
先のデータは、計測のたびに、データキャッシュ、コードキャッシュ両方のデータを
追い出す処理を行っての計測なので、極端かもしれないが、
実際のところ、前回実行時のデータが完全にキャッシュに残っていて
11クロックないし3クロックで関数呼び出し及びリターンが完了するのは
まずありえないであろう。
§5 : 総評
仮想関数は遅いという認識はある意味では正しく、またある意味では正しくない。
少ない回数の呼び出しに於いては、最悪の状況に於ける約800クロックの
オーバーヘッドはほとんど無視できるものであるし、また或いは、
呼び出す関数が小さく、またそのそこでアクセスするメモリの範囲が狭く、
さらに、連続的に呼び出す場合は回数が多くても仮想関数使用に際するオーバーヘッド
は小さいものであると思われる。
しかし、キャッシュのデータが殆ど失われるような関数で、これを多く呼び出す場合
等は、ここが速度のボトルネックなるかもしれない。
(800クロックというのはやはりかなり大きい)
この辺りの影響は単純には推し量れないので、
結局のところ試行錯誤して仮想関数にするか否かを決めるしかないような気も。
(これはソフトウェア設計に大きくかかわるので、パフォーマンスで決められるものでは
無いような気もまた…)
Copyright(C) Hii 2001
(しまった…短くまとめすぎた…)