C++ でインターフェイスを操作するとパフォーマンスが低下しますか?
-
02-07-2019 - |
質問
C++ でインターフェイス (抽象基本クラス) を使用する場合、実行時のパフォーマンスに影響はありますか?
解決
短い答え:いいえ。
長い答え:速度に影響を与えるのは、基本クラスやクラスの階層内にある祖先の数ではありません。唯一のことは、メソッド呼び出しのコストです。
非仮想メソッド呼び出しにはコストがかかります (ただし、インライン化できます)。
仮想メソッド呼び出しは、呼び出す前に呼び出すメソッドを検索する必要があるため、コストが若干高くなります (ただし、これは単純なテーブル検索です) ない 検索)。インターフェイス上のすべてのメソッドは定義上仮想であるため、このコストが発生します。
高速性を重視したアプリケーションを作成している場合を除き、これは問題にはなりません。通常、インターフェイスを使用するとさらに鮮明になるため、知覚される速度の低下が補われます。
他のヒント
仮想ディスパッチを使用して呼び出された関数はインライン化されません
仮想関数には忘れがちなペナルティが 1 つあります。コンパイル時にオブジェクトの型が不明な (一般的な) 状況では、仮想呼び出しはインライン化されません。関数が小さく、インライン展開に適している場合、呼び出しのオーバーヘッドが追加されるだけでなく、コンパイラーが呼び出し関数を最適化する方法にも制限があるため、このペナルティは非常に重大になる可能性があります (仮想関数が一部のレジスタまたはメモリ位置が変更されている場合、呼び出し側と呼び出し先の間で定数値を伝播することはできません)。
仮想通話コストはプラットフォームによって異なります
通常の関数呼び出しと比較した呼び出しオーバーヘッド ペナルティについては、その答えはターゲット プラットフォームによって異なります。x86/x64 CPU を搭載した PC をターゲットにしている場合、最新の x86/x64 CPU は間接呼び出しで分岐予測を実行できるため、仮想関数の呼び出しによるペナルティは非常に小さくなります。ただし、PowerPC またはその他の RISC プラットフォームをターゲットにしている場合、一部のプラットフォームでは間接呼び出しがまったく予測されないため、仮想呼び出しペナルティが非常に大きくなる可能性があります (Cf. PC/Xbox 360 クロスプラットフォーム開発のベスト プラクティス).
通常の呼び出しと比較して、仮想関数呼び出しごとに小さなペナルティが発生します。1 秒あたり何十万回もの呼び出しを実行しない限り、違いが観察される可能性はほとんどありません。いずれにせよ、コードの明瞭性を高めるためには、多くの場合、代償を払う価値があります。
仮想関数を (たとえばインターフェイスを通じて) 呼び出す場合、プログラムはテーブル内の関数を検索して、そのオブジェクトに対してどの関数を呼び出すかを確認する必要があります。これにより、関数を直接呼び出す場合と比較して、小さなペナルティが生じます。
また、仮想関数を使用する場合、コンパイラは関数呼び出しをインライン化できません。したがって、いくつかの小さな関数に対して仮想関数を使用すると、ペナルティが発生する可能性があります。これは通常、発生する可能性が高い最大のパフォーマンス「ヒット」です。これは、関数が小さく、ループ内などから何度も呼び出される場合にのみ問題になります。
場合によっては適用されるもう1つの選択肢は、テンプレートを使用したコンパイルタイム多型です。たとえば、プログラムの開始時に実装の選択を行い、実行中に使用する場合に役立ちます。ランタイム多型の例
class AbstractAlgo
{
virtual int func();
};
class Algo1 : public AbstractAlgo
{
virtual int func();
};
class Algo2 : public AbstractAlgo
{
virtual int func();
};
void compute(AbstractAlgo* algo)
{
// Use algo many times, paying virtual function cost each time
}
int main()
{
int which;
AbstractAlgo* algo;
// read which from config file
if (which == 1)
algo = new Algo1();
else
algo = new Algo2();
compute(algo);
}
コンパイル時ポリモーフィズムを使用した場合と同じ
class Algo1
{
int func();
};
class Algo2
{
int func();
};
template<class ALGO> void compute()
{
ALGO algo;
// Use algo many times. No virtual function cost, and func() may be inlined.
}
int main()
{
int which;
// read which from config file
if (which == 1)
compute<Algo1>();
else
compute<Algo2>();
}
コストの比較は、仮想関数呼び出しと直接関数呼び出しの間ではないと思います。抽象基本クラス (インターフェイス) の使用を検討している場合は、オブジェクトの動的タイプに基づいていくつかのアクションのうちの 1 つを実行する必要がある場合があります。何らかの方法でその選択をしなければなりません。1 つのオプションは、仮想関数を使用することです。もう 1 つは、RTTI (コストがかかる可能性がある) または基本クラスに type() メソッドを追加する (各オブジェクトのメモリ使用量が増加する可能性がある) を介して、オブジェクトの型を切り替えることです。したがって、仮想関数呼び出しのコストは、何もしない場合のコストではなく、代替のコストと比較する必要があります。
ほとんどの人は実行時のペナルティに注目していますが、それは当然のことです。
ただし、大規模なプロジェクトに取り組んだ私の経験では、明確なインターフェイスと適切なカプセル化による利点は、速度の向上をすぐに相殺します。モジュラーコードを交換して実装を改善できるため、最終的には大きな利益が得られます。
達成できる距離は異なる場合があり、開発しているアプリケーションによって明らかに異なります。
注意すべき点の 1 つは、仮想関数呼び出しのコストがプラットフォームごとに異なる可能性があることです。コンソールでは、通常、vtable 呼び出しはキャッシュ ミスを意味し、分岐予測を台無しにする可能性があるため、これらはより顕著になる可能性があります。
多重継承により、オブジェクト インスタンスが複数の vtable ポインターで肥大化することに注意してください。x86 上の G++ では、クラスに仮想メソッドがあり、基底クラスがない場合、vtable へのポインターが 1 つあります。仮想メソッドを持つ基本クラスが 1 つある場合でも、vtable へのポインターが 1 つあります。仮想メソッドを持つ 2 つの基本クラスがある場合、次のようになります。 二 vtable ポインター 各インスタンスで.
したがって、多重継承 (これが C++ でのインターフェイスの実装です) では、基本クラスにポインター サイズを掛けたオブジェクト インスタンスのサイズを支払うことになります。メモリ フットプリントの増加は、間接的にパフォーマンスに影響を与える可能性があります。
C++ で抽象基本クラスを使用すると、通常、仮想関数テーブルの使用が必須となり、すべてのインターフェイス呼び出しはそのテーブルを通じて検索されます。コストは生の関数呼び出しに比べてわずかであるため、心配する前にそれよりも高速に実行する必要があることを確認してください。
私が知っている唯一の主な違いは、具体的なクラスを使用していないため、インライン化が (はるかに?) 難しくなるということです。
私が考えられる唯一のことは、仮想メソッドの呼び出しは非仮想メソッドよりも少し遅いということです。呼び出しは、 仮想メソッドテーブル.
ただし、これはデザインを台無しにする悪い理由です。より高いパフォーマンスが必要な場合は、より高速なサーバーを使用してください。
仮想関数を含むクラスの場合は、vtable が使用されます。明らかに、vtable などのディスパッチ メカニズムを介してメソッドを呼び出すと、直接呼び出しよりも遅くなりますが、ほとんどの場合は問題なく使用できます。
はい、しかし私の知る限り特筆すべきことは何もありません。パフォーマンスへの影響は、各メソッド呼び出しでの「間接化」が原因です。
ただし、一部のコンパイラは抽象基本クラスから継承するクラス内でメソッド呼び出しをインライン化できないため、実際には使用しているコンパイラに依存します。
確実にしたい場合は、独自のテストを実行する必要があります。
はい、ペナルティがあります。プラットフォームのパフォーマンスを向上させる可能性があるのは、仮想関数を持たない非抽象クラスを使用することです。次に、非仮想関数へのメンバー関数ポインターを使用します。
珍しい視点であることは承知していますが、この問題に言及するだけでも、クラス構造について考えすぎているのではないかと疑ってしまいます。私は、「抽象化レベル」が多すぎるシステムを多く見てきましたが、それだけで、メソッド呼び出しのコストが原因ではなく、不必要な呼び出しを行う傾向が原因で、深刻なパフォーマンスの問題が発生しやすくなってしまいました。これが複数のレベルで発生すると、致命的です。 見てください