私のクラス設計では、抽象クラスと仮想関数を幅広く使用しています。仮想機能がパフォーマンスに影響を与えていると感じました。これは本当ですか?しかし、このパフォーマンスの違いは顕著ではなく、時期尚早の最適化を行っているように見えます。正しい?
私のクラス設計では、抽象クラスと仮想関数を幅広く使用しています。仮想機能がパフォーマンスに影響を与えていると感じました。これは本当ですか?しかし、このパフォーマンスの違いは顕著ではなく、時期尚早の最適化を行っているように見えます。正しい?
回答:
経験則は次のとおりです。
証明できるまでは、パフォーマンスの問題ではありません。
仮想関数を使用しても、パフォーマンスにはわずかな影響がありますが、アプリケーションの全体的なパフォーマンスに影響を与えることはほとんどありません。パフォーマンスの改善を探すためのより良い場所は、アルゴリズムとI / Oです。
仮想関数(およびその他)について語る優れた記事は、メンバー関数ポインターと最速のC ++デリゲートです。
あなたの質問に興味をそそられたので、私は先に進み、私たちが協力している3GHzの順序付けられたPowerPC CPUでいくつかのタイミングを実行しました。私が実行したテストは、get / set関数を使用して単純な4Dベクトルクラスを作成することでした
class TestVec
{
float x,y,z,w;
public:
float GetX() { return x; }
float SetX(float to) { return x=to; } // and so on for the other three
}
次に、それぞれ1024個のベクトル(L1に収まるほど小さい)を含む3つの配列を設定し、それらを互いに追加するループ(Ax = Bx + Cx)を1000回実行しました。私は、次のように定義機能でこれを実行したinline
、virtual
と、通常の関数呼び出し。結果は次のとおりです。
したがって、この場合(すべてがキャッシュに収まる場合)、仮想関数呼び出しはインライン呼び出しよりも約20倍遅くなりました。しかし、これはどういう意味ですか?ループを通過するたびに正確に3 * 4 * 1024 = 12,288
関数呼び出しが発生したため(1024ベクトルx 4コンポーネントx追加ごとの3呼び出し)、これらの時間は1000 * 12,288 = 12,288,000
関数呼び出しを表します。仮想ループは直接ループより92ms長くかかったため、呼び出しごとの追加のオーバーヘッドは関数ごとに7 ナノ秒でした。
このことから私は結論:はい、仮想関数は、はるかに遅いの直接の機能よりも、とノー、毎秒千万回それらを呼び出すことで、あなたがしている計画がない限り、それは問題ではありません。
Objective-C(すべてのメソッドが仮想)がiPhone の主要言語であり、JavaがAndroidの主要言語である場合、3 GHzデュアルコアタワーでC ++仮想関数を使用するのはかなり安全だと思います。
非常にパフォーマンスが重要なアプリケーション(ビデオゲームなど)では、仮想関数の呼び出しが遅すぎる場合があります。最新のハードウェアでは、パフォーマンスの最大の問題はキャッシュミスです。データがキャッシュにない場合、データが利用可能になるまでに数百サイクルかかる場合があります。
通常の関数呼び出しでは、CPUが新しい関数の最初の命令をフェッチし、それがキャッシュにない場合、命令キャッシュミスが発生する可能性があります。
仮想関数呼び出しは、最初にオブジェクトからvtableポインターをロードする必要があります。これにより、データキャッシュミスが発生する可能性があります。次に、vtableから関数ポインタをロードします。これにより、別のデータキャッシュミスが発生する可能性があります。次に、非仮想関数のように命令キャッシュミスを引き起こす可能性のある関数を呼び出します。
多くの場合、2つの追加のキャッシュミスは問題になりませんが、パフォーマンスが重要なコードのタイトループでは、パフォーマンスが劇的に低下する可能性があります。
Agner Fogの「Optimizing Software in C ++」マニュアルの 44ページから:
関数呼び出しステートメントが常に同じバージョンの仮想関数を呼び出す場合、仮想メンバー関数の呼び出しにかかる時間は、非仮想メンバー関数の呼び出しにかかる時間より数クロック長くなります。バージョンが変更されると、10〜30クロックサイクルの予測ミスペナルティが発生します。仮想関数呼び出しの予測と予測ミスのルールは、switchステートメントの場合と同じです...
switch
。完全に任意のcase
値で、確かに。しかし、すべてcase
のが連続している場合、コンパイラーはこれを最適化してジャンプテーブル(古き良きZ80日を思い出させる)にできる可能性があります。私がvfuncsをに置き換えることをお勧めするわけではありませんswitch
。;)
絶対に。すべてのメソッド呼び出しは、呼び出される前にvtableのルックアップを必要とするため、コンピューターが100Mhzで実行されたとき、それは問題のある方法でした。しかし、今日..私の最初のコンピュータが持っていたよりも多くのメモリを備えた1次レベルのキャッシュを持つ3Ghz CPUで?どういたしまして。メインRAMからメモリを割り当てると、すべての関数が仮想である場合よりも時間がかかります。
これは、すべてのコードが関数に分割され、各関数にスタックの割り当てと関数呼び出しが必要だったため、構造化プログラミングが遅いと人々が言っていた昔のようです!
仮想関数のパフォーマンスへの影響を検討するのに面倒だと思うのは、仮想関数が非常に頻繁に使用され、テンプレートコードでインスタンス化されて、すべてに渡ってしまった場合だけです。それでも、あまり努力はしません!
PSは他の「使いやすい」言語を考えています。それらのメソッドはすべて仮想化されており、現在はクロールしていません。
実行時間の他に、別のパフォーマンス基準があります。Vtableもメモリスペースを占有し、場合によっては回避できます。ATLはテンプレートを使用してコンパイル時の「シミュレートされた動的バインディング」を使用します説明が難しい「静的多型」の効果を得るため。基本的には、派生クラスをパラメーターとして基本クラステンプレートに渡します。そのため、コンパイル時に、基本クラスは、各インスタンスの派生クラスが何であるかを「認識」します。複数の異なる派生クラスを基本型のコレクション(ランタイムポリモーフィズム)に格納することはできませんが、静的な意味で、既存のテンプレートクラスXと同じクラスYを作成する場合は、この種のオーバーライドのフックは、気になるメソッドをオーバーライドするだけで、vtableを持たなくてもクラスXの基本メソッドを取得できます。
メモリフットプリントが大きいクラスでは、単一のvtableポインターのコストはそれほど大きくありませんが、COMの一部のATLクラスは非常に小さいため、実行時のポリモーフィズムが発生しない場合は、vtableを節約する価値があります。
この他のSOの質問も参照してください。
ちなみに、ここに私が見つけた投稿は、CPU時間のパフォーマンスの側面について語っています。
仮想関数がパフォーマンスの問題になる唯一の方法は、多くの仮想関数がタイトループ内で呼び出された場合と、ページフォールトやその他の「重い」メモリ操作が発生した場合のみです。
他の人が言ったように、実際にはあなたにとって問題になることはほとんどありません。そして、そうであると思われる場合は、プロファイラーを実行し、いくつかのテストを実行して、パフォーマンスの利益のためにコードを「設計解除」する前に、これが本当に問題であるかどうかを確認してください。
クラスメソッドが仮想でない場合、コンパイラは通常インライン化します。逆に、仮想関数を持つクラスへのポインタを使用する場合、実際のアドレスは実行時にのみ認識されます。
これはテストによってよく示されています、時間差〜700%(!):
#include <time.h>
class Direct
{
public:
int Perform(int &ia) { return ++ia; }
};
class AbstrBase
{
public:
virtual int Perform(int &ia)=0;
};
class Derived: public AbstrBase
{
public:
virtual int Perform(int &ia) { return ++ia; }
};
int main(int argc, char* argv[])
{
Direct *pdir, dir;
pdir = &dir;
int ia=0;
double start = clock();
while( pdir->Perform(ia) );
double end = clock();
printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );
Derived drv;
AbstrBase *ab = &drv;
ia=0;
start = clock();
while( ab->Perform(ia) );
end = clock();
printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );
return 0;
}
仮想関数呼び出しの影響は状況に大きく依存します。関数内の呼び出しが少なく、かなりの量の作業がある場合は、無視できます。
または、いくつかの単純な操作をしながら、何度も繰り返し使用される仮想呼び出しである場合、それは非常に大きくなる可能性があります。
++ia
ます。だから何?
私は私の特定のプロジェクトでこれを少なくとも20回行ったり来たりしました。コードの再利用、明快さ、保守性、読みやすさの点でいくつかの大きな利点がある可能性がありますが、その一方で、仮想関数では依然としてパフォーマンスへの影響が存在します。
最近のラップトップ/デスクトップ/タブレットでパフォーマンスのヒットが目立つようになるでしょうか?ただし、組み込みシステムの特定のケースでは、特に仮想関数がループで何度も呼び出される場合、パフォーマンスの打撃がコードの非効率性の原動力になることがあります。
:ここでは、組込みシステムのコンテキストでC / C ++のためのベストプラクティスをanaylzes時代遅れの一部、何紙だhttp://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdfは、
結論としては、特定のコンストラクトを別のコンストラクトで使用することの長所/短所を理解するのはプログラマの責任です。スーパーパフォーマンスを重視しているのでない限り、パフォーマンスへの影響を気にする必要はないでしょう。コードをできるだけ使いやすくするために、C ++のきちんとしたオブジェクト指向の要素をすべて使用する必要があります。
私の経験では、主な関連事項は、関数をインライン化する機能です。関数をインライン化する必要があることを示すパフォーマンス/最適化のニーズがある場合、それを防ぐため、関数を仮想化することはできません。そうでなければ、おそらく違いに気付かないでしょう。
注意すべき点の1つは、次のとおりです。
boolean contains(A element) {
for (A current: this)
if (element.equals(current))
return true;
return false;
}
これより速いかもしれません:
boolean contains(A element) {
for (A current: this)
if (current.equals(equals))
return true;
return false;
}
これは、最初のメソッドが1つの関数を呼び出すだけで、2番目のメソッドが多くの異なる関数を呼び出す可能性があるためです。これは、あらゆる言語のあらゆる仮想機能に適用されます。
これはコンパイラ、キャッシュなどに依存するため、「可能」と言います。
仮想関数を使用することによるパフォーマンスの低下は、設計レベルで得られる利点を上回ることはありません。仮想関数の呼び出しは、静的関数の直接呼び出しよりも25%効率が悪いと思われます。これは、VMTを介した間接参照のレベルがあるためです。ただし、呼び出しを実行するのにかかる時間は、通常、関数の実際の実行にかかる時間と比較して非常に短いため、特にハードウェアの現在のパフォーマンスでは、総パフォーマンスコストは無視できます。さらに、コンパイラーは時々最適化し、仮想呼び出しが不要であることを確認して、静的呼び出しにコンパイルすることができます。したがって、仮想関数と抽象クラスを必要なだけ使用することを心配しないでください。
The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.
主な違いは言っているのsometimes
ではなく、あなたがそう言ったのに同意したかもしれませんnever
。
特に-数年前-標準のメンバーメソッド呼び出しと仮想呼び出しのタイミングを比較するこのようなテストも行ったので、私はいつもこれに疑問を投げかけていました。そのときの結果には本当に怒っていました。非仮想よりも8倍遅い。
今日、非常にパフォーマンスが重要なアプリで、バッファークラスにメモリを割り当てるために仮想関数を使用するかどうかを決定する必要があったので、ググって(そしてあなたを見つけて)、最後にもう一度テストを行いました。
// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo> // typeid
#include <cstdio> // printf
#include <cstdlib> // atoll
#include <ctime> // clock_gettime
struct Virtual { virtual int call() { return 42; } };
struct Inline { inline int call() { return 42; } };
struct Normal { int call(); };
int Normal::call() { return 42; }
template<typename T>
void test(unsigned long long count) {
std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);
timespec t0, t1;
clock_gettime(CLOCK_REALTIME, &t0);
T test;
while (count--) test.call();
clock_gettime(CLOCK_REALTIME, &t1);
t1.tv_sec -= t0.tv_sec;
t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
? t1.tv_nsec - t0.tv_nsec
: 1000000000lu - t0.tv_nsec;
std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}
template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
test<T>(count);
test<Ua, Un...>(count);
}
int main(int argc, const char* argv[]) {
test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
return 0;
}
そして、それが実際にはまったく問題にならないことに本当に驚いた。インラインが非仮想よりも速く、仮想よりも高速であることは理にかなっていますが、キャッシュに必要なデータがあるかどうかにかかわらず、最適化できる可能性がある一方で、コンピューター全体の負荷になることがよくあります。キャッシュレベルでは、これはアプリケーション開発者よりもコンパイラ開発者が行うべきだと思います。