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


125

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


私の回答によれば、stackoverflow.com
Suma


2
ハイパフォーマンスコンピューティングと数値計算を行う場合は、計算のコアに仮想性を使用しないでください。すべてのパフォーマンスが確実に失われ、コンパイル時の最適化が妨げられます。プログラムの初期化やファイナライズには重要ではありません。インターフェースを操作するときは、必要に応じて仮想性を使用できます。
Vincent

回答:


90

経験則は次のとおりです。

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

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

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


純粋な仮想関数はどうですか?パフォーマンスに何らかの影響がありますか?彼らは単に実装を強制するためにそこにあるように見えるのでただ疑問に思っています。
thomthom 2013

2
@thomthom:正解です。純粋な仮想関数と通常の仮想関数の間にパフォーマンスの違いはありません。
グレッグヒューギル

168

あなたの質問に興味をそそられたので、私は先に進み、私たちが協力している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回実行しました。私は、次のように定義機能でこれを実行したinlinevirtualと、通常の関数呼び出し。結果は次のとおりです。

  • インライン:8ms(1コールあたり0.65ns)
  • 直接:68ms(1コールあたり5.53ns)
  • 仮想:160ms(1コールあたり13ns)

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

このことから私は結論:はい、仮想関数は、はるかに遅いの直接の機能よりも、とノー、毎秒千万回それらを呼び出すことで、あなたがしている計画がない限り、それは問題ではありません。

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


ただし、複数回呼び出された場合は、1回しか呼び出されない場合よりも安くなることがよくあります。私の無関係なブログ:phresnel.org/blogの「仮想機能は有害ではないと考えられている」というタイトルの投稿を参照してください。もちろん、それはコードパスの複雑さに依存します
Sebastian Mach

22
テストでは、繰り返し呼び出される仮想関数の小さなセットを測定します。ブログの投稿では、コードの時間コストは操作をカウントすることで測定できると想定していますが、常にそうであるとは限りません。最新のプロセッサでのvfuncの主なコストは、分岐の予測ミスによって引き起こされるパイプラインのバブルです。
Crashworks、

10
これは、gcc LTO(リンク時最適化)の優れたベンチマークになります。ltoを有効にしてこれを再度コンパイルしてみてください:gcc.gnu.org/wiki/LinkTimeOptimizationと20xファクターで何が起こるかを確認してください
lurscher

1
クラスに1つの仮想関数と1つのインライン関数がある場合、非仮想メソッドのパフォーマンスも影響を受けますか?単にクラスが仮想的であるという性質によって?
thomthom 2014

4
@thomthomいいえ、virtual / non-virtualは関数ごとの属性です。関数は、仮想としてマークされている場合、または仮想としてそれを持っている基本クラスをオーバーライドしている場合にのみ、vtableを介して定義する必要があります。多くの場合、パブリックインターフェイス用の仮想関数のグループを持つクラスが表示され、次に多くのインラインアクセサーなどが表示されます。(技術的には、これは実装固有であり、コンパイラがさえ「インライン」と記された機能のための仮想pontersを使用することができますが、そのようなコンパイラを書いた人は非常識でしょう。)
Crashworks

42

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


4
iPhoneがパフォーマンスコードの良い例であることはわかりません:youtube.com/watch
v

13
@Crashworks:iPhoneはコードの例ではありません。これはハードウェアの例です。具体的には、低速のハードウェアです。これらの「遅い」言語がパワー不足のハードウェアに十分対応できる場合、仮想機能は大きな問題にはなりません。
Chuck

52
iPhoneはARMプロセッサで動作します。iOSで使用されるARMプロセッサは、低MHzおよび低電力での使用向けに設計されています。CPUには分岐予測用のシリコンがないため、仮想関数呼び出しによる分岐予測ミスによるパフォーマンスオーバーヘッドはありません。また、iOSハードウェアのMHzは十分に低いため、RAMからデータを取得している間、キャッシュミスによってプロセッサが300クロックサイクル間ストールしません。キャッシュミスは、低いMHzではそれほど重要ではありません。つまり、iOSデバイスで仮想関数を使用することによるオーバーヘッドはありませんが、これはハードウェアの問題であり、デスクトップCPUには当てはまりません。
HaltingState、2011年

4
長い間Javaプログラマーとして新しくC ++を使用していたので、JavaのJITコンパイラーとランタイムオプティマイザーは、事前定義された数のループの後、ランタイムで一部の関数をコンパイル、予測、さらにはインライン化する機能を追加したいと思います。ただし、ランタイムコールパターンがないため、C ++にコンパイル時およびリンク時にこのような機能があるかどうかはわかりません。したがって、C ++では、もう少し注意する必要があるかもしれません。
Alex Suo

@AlexSuo私はあなたのポイントがわからないのですか?コンパイルされているため、C ++はもちろん実行時に発生する可能性のあることに基づいて最適化することはできません。したがって、予測などはCPU自体で行う必要があります...しかし、優れたC ++コンパイラ(指示されている場合)は、関数とループを最適化するために非常に長くなりますランタイム。
underscore_d

34

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

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

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

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


6
そうですが、タイトなループから繰り返し呼び出されるコード(またはvtable)が(もちろん)キャッシュミスに陥ることはめったにありません。さらに、vtableポインターは通常、呼び出されたメソッドがアクセスするオブジェクト内の他のデータと同じキャッシュラインにあるため、1つの余分なキャッシュミスのみについて話していることがよくあります。
Qwertie

5
@Qwertie私はそれが必要であるとは思わない。ループの本体(L1キャッシュよりも大きい場合)はvtableポインター、関数ポインターを「リタイア」する可能性があり、その後の反復では、反復ごとにL2キャッシュ(またはそれ以上)のアクセスを待機する必要があります
Ghita

30

Agner Fogの「Optimizing Software in C ++」マニュアルの 44ページから:

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


この参照をありがとう。Agner Fogの最適化マニュアルは、ハードウェアを最適に利用するためのゴールドスタンダードです。
Arto Bendiken 2013年

私の思い出とクイック検索に基づいて-stackoverflow.com/questions/17061967/c-switch-and-jump-tables-これは常に当てはまるとは思えませんswitch。完全に任意のcase値で、確かに。しかし、すべてcaseのが連続している場合、コンパイラーはこれを最適化してジャンプテーブル(古き良きZ80日を思い出させる)にできる可能性があります。私がvfuncsをに置き換えることをお勧めするわけではありませんswitch。;)
underscore_d

7

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

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

仮想関数のパフォーマンスへの影響を検討するのに面倒だと思うのは、仮想関数が非常に頻繁に使用され、テンプレートコードでインスタンス化されて、すべてに渡ってしまった場合だけです。それでも、あまり努力はしません!

PSは他の「使いやすい」言語を考えています。それらのメソッドはすべて仮想化されており、現在はクロールしていません。


4
まあ、今日でも関数呼び出しを回避することは、高パフォーマンスのアプリにとって重要です。違いは、今日のコンパイラーは小さな関数を確実にインライン化するため、小さな関数を作成する際の速度の低下に悩まされないことです。仮想関数に関しては、スマートCPUはそれらに対してスマート分岐予測を実行できます。古いコンピューターの方が遅いというのは、問題ではないと私は思います。そう、それらはずっと遅いのですが、当時はそれがわかっていたので、はるかに小さなワークロードを与えました。1992年にMP3をプレイした場合、CPUの半分以上をそのタスク専用にする必要があるかもしれません。
Qwertie

6
mp3の日付は1995年です。92年には386がほとんどなく、mp3を再生する方法がありませんでした。CPU時間の50%は、優れたマルチタスクOS、アイドルプロセス、およびプリエンプティブスケジューラを想定しています。当時、これは消費者市場には存在しませんでした。電源が入った瞬間から100%、話は終わり。
v.oddou 2014年

7

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

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

この他のSOの質問も参照してください。

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



4

はい、そうです。仮想関数呼び出しのコストについて知りたい場合は、この投稿が興味深いかもしれません。


1
リンクされた記事は、仮想呼び出しの非常に重要な部分を考慮していないため、ブランチが誤って予測される可能性があります。
スマ

4

仮想関数がパフォーマンスの問題になる唯一の方法は、多くの仮想関数がタイトループ内で呼び出された場合と、ページフォールトやその他の「重い」メモリ操作が発生した場合のみです。

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


2
タイトなループで何かを呼び出すと、コードとデータがすべてキャッシュ内でホットに保たれる可能性があります...
グレッグロジャース

2
はい。ただし、その右ループがオブジェクトのリストを繰り返し処理している場合、各オブジェクトが同じ関数呼び出しを介して異なるアドレスで仮想関数を呼び出している可能性があります。
Daemin

3

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

これはテストによってよく示されています、時間差〜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;
}

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

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


4
仮想関数呼び出しは、に比べてコストがかかり++iaます。だから何?
Bo Persson、2012

2

私は私の特定のプロジェクトでこれを少なくとも20回行ったり来たりしました。コードの再利用、明快さ、保守性、読みやすさの点でいくつかの大きな利点がある可能性がありますが、その一方で、仮想関数で依然としてパフォーマンスへの影響が存在します。

最近のラップトップ/デスクトップ/タブレットでパフォーマンスのヒットが目立つようになるでしょうか?ただし、組み込みシステムの特定のケースでは、特に仮想関数がループで何度も呼び出される場合、パフォーマンスの打撃がコードの非効率性の原動力になることがあります。

:ここでは、組込みシステムのコンテキストでC / C ++のためのベストプラクティスをanaylzes時代遅れの一部、何紙だhttp://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdfは、

結論としては、特定のコンストラクトを別のコンストラクトで使用することの長所/短所を理解するのはプログラマの責任です。スーパーパフォーマンスを重視しているのでない限り、パフォーマンスへの影響を気にする必要はないでしょう。コードをできるだけ使いやすくするために、C ++のきちんとしたオブジェクト指向の要素をすべて使用する必要があります。


2

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


1

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

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


0

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


2
ターゲットコンピュータがどれほど小さくても、
zumalifeguard 2015年

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
underscore_d

-1

特に-数年前-標準のメンバーメソッド呼び出しと仮想呼び出しのタイミングを比較するこのようなテストも行ったので、私はいつもこれに疑問を投げかけていました。そのときの結果には本当に怒っていました。非仮想よりも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;
}

そして、それが実際にはまったく問題にならないことに本当に驚いた。インラインが非仮想よりも速く、仮想よりも高速であることは理にかなっていますが、キャッシュに必要なデータがあるかどうかにかかわらず、最適化できる可能性がある一方で、コンピューター全体の負荷になることがよくあります。キャッシュレベルでは、これはアプリケーション開発者よりもコンパイラ開発者が行うべきだと思います。


12
あなたのコンパイラーは、あなたのコードの仮想関数呼び出しがVirtual :: callしか呼び出せないと言うことができると思います。その場合、インライン化できます。また、コンパイラーがNormal :: callのインライン化を妨げるものは何もありません。したがって、コンパイラーが3つの操作に対して同一のコードを生成しているため、3つの操作で同じ時間が得られる可能性はかなり高いと思います。
Bjarke H. Roune、2011
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.