web-dev-qa-db-ja.com

仮想関数とパフォーマンス-C ++

私のクラス設計では、抽象クラスと仮想関数を広範囲に使用しています。仮想機能がパフォーマンスに影響を与えると感じました。これは本当ですか?しかし、このパフォーマンスの違いは目立たず、私は時期尚早な最適化を行っているようです。右?

114
Navaneeth K N

目安は次のとおりです。

証明できるまでパフォーマンスの問題ではありません。

仮想関数の使用はパフォーマンスにごくわずかな影響を及ぼしますが、アプリケーションの全体的なパフォーマンスに影響を与えることはほとんどありません。パフォーマンスの改善を探すのに適した場所は、アルゴリズムとI/Oです。

仮想関数(およびその他)について説明している優れた記事は、 メンバー関数ポインターと最速のC++デリゲート です。

86
Greg Hewgill

あなたの質問に私は興味をそそられたので、先に進んで、動作する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 
}

次に、これらのベクトル(L1に収まるほど小さい)をそれぞれ1024個含む3つの配列をセットアップし、それらを互いに追加するループを実行しました(A.x = B.x + C.x)。 inlinevirtual、および通常の関数呼び出しとして定義された関数でこれを実行しました。結果は次のとおりです。

  • インライン:8ms(呼び出しごとに0.65ns)
  • 直接:68ms(コールごとに5.53ns)
  • 仮想:160ms(コールごとに13ns)

したがって、この場合(すべてがキャッシュに収まる場合)、仮想関数呼び出しはインライン呼び出しよりも約20倍遅くなりました。しかし、これは本当に何を意味するのでしょうか?ループを通過するたびに3 * 4 * 1024 = 12,288関数呼び出し(1024ベクトルx 4コンポーネントx加算ごとの3呼び出し)が発生したため、これらの時間は1000 * 12,288 = 12,288,000関数呼び出しを表します。仮想ループは直接ループよりも92ミリ秒長くかかったため、呼び出しごとの追加オーバーヘッドは関数ごとに7ナノ秒でした。

これから私は結論づけます:yes、仮想関数は直接関数よりもずっと遅く、no、1秒間に1,000万回呼び出すことを計画していない限り、問題ではありません。

参照: 生成されたアセンブリの比較

162
Crashworks

Objective-C(すべてのメソッドが仮想)がiPhoneの主要言語であり、Androidのメイン言語がJavaの場合、3 GHzでC++仮想関数を使用するのはかなり安全だと思いますデュアルコアタワー。

42
Chuck

パフォーマンスが非常に重要なアプリケーション(ビデオゲームなど)では、仮想関数呼び出しが遅すぎる場合があります。最新のハードウェアでは、パフォーマンスの最大の懸念はキャッシュミスです。データがキャッシュにない場合、利用可能になるまでに数百サイクルかかる場合があります。

CPUが新しい関数の最初の命令をフェッチし、それがキャッシュにない場合、通常の関数呼び出しは命令キャッシュミスを生成する可能性があります。

仮想関数呼び出しは、最初にオブジェクトからvtableポインターをロードする必要があります。これにより、データキャッシュミスが発生する可能性があります。次に、vtableから関数ポインターをロードします。これにより、別のデータキャッシュミスが発生する可能性があります。その後、非仮想関数のように命令キャッシュミスが発生する可能性のある関数を呼び出します。

多くの場合、2回の余分なキャッシュミスは問題になりませんが、パフォーマンスが重要なコードのタイトループでは、パフォーマンスが劇的に低下する可能性があります。

34
Mark James

Agner Fogの「C++でのソフトウェアの最適化」マニュアル の44ページから:

関数呼び出しステートメントが常に同じバージョンの仮想関数を呼び出す場合、仮想メンバー関数の呼び出しにかかる時間は、非仮想メンバー関数の呼び出しにかかる時間よりも数クロックサイクル長くなります。バージョンが変更されると、10〜30クロックサイクルの予測ミスのペナルティが発生します。仮想関数呼び出しの予測と予測ミスのルールは、switchステートメントと同じです...

27
Boojum

絶対に。すべてのメソッド呼び出しは、vtableが呼び出される前にvtableをルックアップする必要があったため、コンピューターが100Mhzで実行されたとき、それは問題のある方法でした。しかし、今日..私の最初のコンピューターが持っていたよりも多くのメモリを備えた1次キャッシュを備えた3Ghz CPU上で?どういたしまして。 main RAMからメモリを割り当てると、すべての関数が仮想である場合よりも時間がかかります。

すべてのコードが関数に分割され、各関数がスタックの割り当てと関数呼び出しを必要としたため、構造化プログラミングが遅いと人々が言っ​​た昔のように!

仮想関数のパフォーマンスへの影響を検討することさえ考えているのは、それが非常に頻繁に使用され、テンプレートコードでインスタンス化され、すべてが終了した場合だけです。それでも、あまり労力を費やすことはありません!

PSは他の「使いやすい」言語を考えています-それらのメソッドはすべて隠れて仮想であり、最近はクロールしません。

7
gbjbaanb

実行時間の他に、別のパフォーマンス基準があります。 Vtableもメモリ空間を占有し、場合によっては回避できます。ATLはコンパイル時の " simulated dynamic binding " with templates を使用して "staticポリモーフィズム」は、説明するのが難しいです。基本的に、派生クラスをパラメーターとして基本クラステンプレートに渡すため、コンパイル時に基本クラスは、各インスタンスで派生クラスが何であるかを「認識」します。複数の異なる派生クラスを基本型のコレクション(ランタイムポリモーフィズム)に格納させませんが、静的な意味で、既存のテンプレートクラスXと同じクラスYを作成する場合は、この種のオーバーライドのフックは、気にするメソッドをオーバーライドするだけでよく、それからvtableを持たずにクラスXのベースメソッドを取得します。

メモリフットプリントが大きいクラスでは、単一のvtableポインターのコストはそれほど大きくありませんが、COMのATLクラスの一部は非常に小さく、実行時ポリモーフィズムのケースが発生しない場合、vtableを節約する価値があります。

this other SO question もご覧ください。

ちなみに 私が見つけた投稿 はCPU時間のパフォーマンスの側面について述べています。

6
Jason S

はい、あなたは正しいですし、仮想関数呼び出しのコストに興味があるなら、 この投稿 おもしろいと思うかもしれません。

4
Serge

仮想関数がパフォーマンスの問題になることを確認できる唯一の方法は、多くの仮想関数がタイトループ内で呼び出され、if and onlyがページフォールトまたは他の「重い」発生するメモリ操作。

他の人が言ったように、実際の生活ではほとんど問題にならないでしょう。そして、そうだと思うなら、プロファイラーを実行し、いくつかのテストを実行し、パフォーマンスの向上のためにコードを「設計解除」しようとする前に、これが本当に問題かどうかを確認します。

3
Daemin

クラスメソッドが仮想でない場合、コンパイラは通常インライン化を行います。それどころか、仮想関数であるクラスへのポインタを使用すると、実際のアドレスは実行時にのみ認識されます。

これはテストでよく示されており、時差〜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;
}

仮想関数呼び出しの影響は状況に大きく依存します。呼び出しがほとんどなく、関数内でかなりの量の作業がある場合は、無視してもかまいません。

または、いくつかの簡単な操作を行いながら何度も繰り返し使用される仮想呼び出しの場合は、非常に大きくなる可能性があります。

3
Evgueny Sedov

特定のプロジェクトでこれを少なくとも20回繰り返しました。 はコードの再利用、明快さ、保守性、読みやすさの点でいくつかの大きなメリットがありますが、一方で、パフォーマンスの低下はまだありますdoは仮想関数に存在します。

パフォーマンスの低下は、最新のラップトップ/デスクトップ/タブレットで顕著になりますか?ただし、組み込みシステムの特定のケースでは、特に仮想関数がループ内で何度も呼び出される場合、パフォーマンスの低下がコードの非効率性の要因になる場合があります。

以下は、組み込みシステムのコンテキストにおけるC/C++のベストプラクティスを分析した、やや時代遅れの論文です。 http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper pdf

結論として、特定のコンストラクトを別のコンストラクトに使用することの長所/短所を理解するのはプログラマー次第です。あなたがパフォーマンスを重視しているのでなければ、おそらくパフォーマンスの低下を気にせず、C++のすてきなOOをすべて使用して、コードをできるだけ使いやすくする必要があります。

2
It'sPete

私の経験では、主な関連事項は関数をインライン化する能力です。関数をインライン化する必要があることを示すパフォーマンス/最適化のニーズがある場合、関数を仮想化することはできません。そうでなければ、おそらく違いに気付かないでしょう。

2
Hurkyl

注意すべきことは、次のことです。

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番目のメソッドは多くの異なる関数を呼び出している可能性があるためです。これは、あらゆる言語のあらゆる仮想機能に適用されます。

これはコンパイラ、キャッシュなどに依存するため、「may」と言います。

1
nikdeapen

仮想関数を使用することによるパフォーマンスの低下は、設計レベルで得られる利点を上回ることはありません。おそらく、仮想関数の呼び出しは、静的関数の直接呼び出しよりも25%効率が悪いでしょう。これは、VMTを通じて間接的なレベルがあるためです。ただし、呼び出しにかかる時間は通常、関数の実際の実行にかかる時間と比較して非常に短いため、特にハードウェアの現在のパフォーマンスでは、総パフォーマンスコストは無視できます。さらに、コンパイラは、仮想呼び出しが不要であることを最適化して確認し、静的呼び出しにコンパイルすることができます。したがって、必要に応じて仮想関数と抽象クラスを使用する心配はありません。

0
Koen