誰でも詳細に、仮想テーブルが正確にどのように機能するか、仮想関数が呼び出されたときにどのポインターが関連付けられるかを詳細に説明できますか。
実際に遅い場合、仮想関数の実行にかかる時間が通常のクラスメソッドよりも長いことを示すことができますか?いくつかのコードを見なくても、どのように/何が起こっているのかを簡単に見失うことがあります。
誰でも詳細に、仮想テーブルが正確にどのように機能するか、仮想関数が呼び出されたときにどのポインターが関連付けられるかを詳細に説明できますか。
実際に遅い場合、仮想関数の実行にかかる時間が通常のクラスメソッドよりも長いことを示すことができますか?いくつかのコードを見なくても、どのように/何が起こっているのかを簡単に見失うことがあります。
回答:
仮想メソッドは、通常、いわゆる仮想メソッドテーブル(略してvtable)を介して実装されます。このテーブルには、関数ポインターが格納されます。これにより、実際の呼び出しに間接性が追加されます(vtableから呼び出す関数のアドレスを取得してから呼び出します-すぐに呼び出すのではなく)。もちろん、これにはある程度の時間とコードが必要です。
ただし、それが必ずしも速度低下の主な原因とは限りません。本当の問題は、コンパイラーが(一般的に/通常)呼び出される関数を知ることができないことです。そのため、インライン化することも、そのような最適化を実行することもできません。これだけでも、無数の無意味な命令(レジスタの準備、呼び出し、その後の状態の復元)が追加される可能性があり、一見無関係な他の最適化が禁止される可能性があります。さらに、多くの異なる実装を呼び出すことで狂ったように分岐すると、他の手段で狂ったように分岐するのと同じヒットになります:キャッシュと分岐予測子はあなたを助けません、分岐は完全に予測可能なものよりも時間がかかりますブランチ。
大きいが、これらのパフォーマンスヒットは通常、問題にはなりません。高性能なコードを作成し、警告頻度で呼び出される仮想関数を追加することを検討する場合は、検討する価値があります。しかし、また、分岐の他の手段で仮想関数呼び出しを交換する(ことに注意してくださいif .. else
、switch
、関数ポインタなど)が根本的な問題を解決することはできません-それは非常によく遅くなることがあります。問題(存在する場合)は、仮想関数ではなく、(不要な)間接化です。
編集:呼び出し指示の違いは他の回答で説明されています。基本的に、静的(「通常」)呼び出しのコードは次のとおりです。
仮想呼び出しは、コンパイル時に関数アドレスが不明であることを除いて、まったく同じことを行います。代わりに、いくつかの指示...
ブランチについて:ブランチは、次の命令を実行させるのではなく、別の命令にジャンプするものです。これには、、さまざまなループの一部、関数呼び出しなどが含まれif
、switch
コンパイラは実際には内部で分岐を必要とする方法で分岐していないように見えるものを実装します。参照してくださいなぜ、ソートされていない配列よりも速くソートされた配列を処理していますか?これが遅い理由、この速度低下に対処するためにCPUが行うこと、およびこれが万能薬ではない理由について。
以下は、それぞれ仮想関数呼び出しと非仮想呼び出しから実際に逆アセンブルされたコードです。
mov -0x8(%rbp),%rax
mov (%rax),%rax
mov (%rax),%rax
callq *%rax
callq 0x4007aa
仮想呼び出しには、正しいアドレスを検索するために3つの追加の命令が必要ですが、非仮想呼び出しのアドレスはコンパイルできます。
ただし、ほとんどの場合、余分なルックアップ時間は無視できると見なされることに注意してください。ループのようにルックアップ時間が非常に長い状況では、通常、ループの前に最初の3つの命令を実行することで値をキャッシュできます。
ルックアップ時間が重要になるもう1つの状況は、オブジェクトのコレクションがあり、各オブジェクトで仮想関数の呼び出しをループしている場合です。ただし、その場合は、とにかく呼び出す関数を選択する何らかの手段が必要になります。仮想テーブルのルックアップは他の手段と同じくらい良い手段です。実際、vtableルックアップコードは非常に広く使用されているため、大幅に最適化されているため、手動で回避しようとすると、パフォーマンスが低下する可能性が高くなります。
-0x8(%rbp)
。ああ... AT&T構文。
何より遅い?
仮想関数は、直接的な関数呼び出しでは解決できない問題を解決します。一般に、同じことを計算する2つのプログラムのみを比較できます。「このレイトレーサーはそのコンパイラよりも高速です」というのは意味がありません。この原則は、個々の関数やプログラミング言語の構成要素のような小さなものにも一般化されます。
仮想関数を使用して、オブジェクトのタイプなどのデータムに基づいたコードに動的に切り替えない場合switch
、同じことを達成するためにステートメントなどの別のものを使用する必要があります。他の何かに独自のオーバーヘッドがあり、さらにプログラムの組織に影響を与え、保守性とグローバルなパフォーマンスに影響を与えます。
C ++では、仮想関数の呼び出しは常に動的ではないことに注意してください。正確な型がわかっているオブジェクトに対して呼び出しが行われた場合(オブジェクトがポインターまたは参照ではないため、またはその型が静的に推測されるため)、呼び出しは単なる通常のメンバー関数呼び出しです。これは、ディスパッチのオーバーヘッドがないことを意味するだけでなく、これらの呼び出しを通常の呼び出しと同じ方法でインライン化できることも意味します。
つまり、C ++コンパイラは、仮想関数が仮想ディスパッチを必要としない場合に動作するため、通常、非仮想関数と比較してパフォーマンスを心配する必要はありません。
新規:また、共有ライブラリを忘れてはなりません。共有ライブラリにあるクラスを使用している場合、通常のメンバー関数の呼び出しは、のような素敵な1つの命令シーケンスではありませんcallq 0x4007aa
。「プログラムリンクテーブル」またはそのような構造を介して間接的に行うなど、いくつかのフープを経る必要があります。したがって、共有ライブラリの間接化は、(完全にではないにしても)ある程度(完全に間接的な)仮想呼び出しと直接呼び出しのコスト差を平準化できます。そのため、仮想関数のトレードオフについての推論では、プログラムの構築方法を考慮する必要があります。ターゲットオブジェクトのクラスが、呼び出しを行っているプログラムにモノリシックにリンクされているかどうか。