1

仮想関数呼び出しは、v-table への余分なインデックス付きの参照を必要とする仮想関数呼び出しが原因で遅くなる可能性があり、その結果、データ キャッシュ ミスと命令キャッシュ ミスが発生する可能性があります...パフォーマンスが重要なアプリケーションには適していません。

そこで私は、仮想関数のこのパフォーマンスの問題を克服しつつ、仮想関数が提供する機能と同じ機能をいくつか維持する方法を考えてきました。

これは以前にも行われたと確信していますが、基本クラスが派生クラスによって設定できるメンバー関数ポインターを格納できるようにする簡単なテストを考案しました。そして、派生クラスで Foo() を呼び出すと、v-table をトラバースしなくても適切なメンバー関数が呼び出されます...

この方法が仮想呼び出しパラダイムの実行可能な代替品であるかどうか疑問に思っています。

お時間をいただきありがとうございます。:)

class BaseClass
{
protected:

    // member function pointer
    typedef void(BaseClass::*FooMemFuncPtr)();
    FooMemFuncPtr m_memfn_ptr_Foo;

    void FooBaseClass() 
    {
        printf("FooBaseClass() \n");
    }

public:

    BaseClass()
    {
        m_memfn_ptr_Foo = &BaseClass::FooBaseClass;
    }

    void Foo()
    {
        ((*this).*m_memfn_ptr_Foo)();
    }
};

class DerivedClass : public BaseClass
{
protected:

    void FooDeriveddClass()
    {
        printf("FooDeriveddClass() \n");
    }

public:

    DerivedClass() : BaseClass()
    {
        m_memfn_ptr_Foo = (FooMemFuncPtr)&DerivedClass::FooDeriveddClass;
    }
};

int main(int argc, _TCHAR* argv[])
{
    DerivedClass derived_inst;
    derived_inst.Foo(); // "FooDeriveddClass()"

    BaseClass base_inst;
    base_inst.Foo(); // "FooBaseClass()"

    BaseClass * derived_heap_inst = new DerivedClass;
    derived_heap_inst->Foo();

    return 0;
}
4

5 に答える 5

3

テストを行ったところ、仮想関数呼び出しを使用するバージョンは、最適化されたシステムでより高速でした。

$ time ./main 1
Using member pointer

real    0m3.343s
user    0m3.340s
sys     0m0.002s

$ time ./main 2
Using virtual function call

real    0m2.227s
user    0m2.219s
sys     0m0.006s

コードは次のとおりです。

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <stdio.h>

struct BaseClass
{
    typedef void(BaseClass::*FooMemFuncPtr)();
    FooMemFuncPtr m_memfn_ptr_Foo;

    void FooBaseClass() { }

    BaseClass()
    {
        m_memfn_ptr_Foo = &BaseClass::FooBaseClass;
    }

    void Foo()
    {
        ((*this).*m_memfn_ptr_Foo)();
    }
};

struct DerivedClass : public BaseClass
{
    void FooDerivedClass() { }

    DerivedClass() : BaseClass()
    {
        m_memfn_ptr_Foo = (FooMemFuncPtr)&DerivedClass::FooDerivedClass;
    }
};

struct VBaseClass {
  virtual void Foo() = 0;
};

struct VDerivedClass : VBaseClass {
  virtual void Foo() { }
};

static const size_t count = 1000000000;

static void f1(BaseClass* bp)
{
  for (size_t i=0; i!=count; ++i) {
    bp->Foo();
  }
}

static void f2(VBaseClass* bp)
{
  for (size_t i=0; i!=count; ++i) {
    bp->Foo();
  }
}

int main(int argc, char** argv)
{
    int test = atoi(argv[1]);
    switch (test) {
        case 1:
        {
            std::cerr << "Using member pointer\n";
            DerivedClass d;
            f1(&d);
            break;
        }
        case 2:
        {
            std::cerr << "Using virtual function call\n";
            VDerivedClass d;
            f2(&d);
            break;
        }
    }

    return 0;
}

以下を使用してコンパイル:

g++ -O2    main.cpp   -o main

g++ 4.7.2 で。

于 2013-06-27T14:02:27.537 に答える
2

仮想関数呼び出しは、v テーブルをトラバースする必要があるため、遅くなる可能性があります。

それはあまり正しくありません。vtable はオブジェクトの構築時に計算され、各仮想関数ポインターは階層内で最も特殊化されたバージョンに設定されます。仮想関数を呼び出すプロセスは、ポインターを反復するのではなく*(vtbl_address + 8)(args);、定数時間で計算される のようなものを呼び出します。

これにより、命令キャッシュ ミスだけでなくデータ キャッシュ ミスも発生する可能性があります。パフォーマンスが重要なアプリケーションには適していません。

あなたのソリューションは一般的であるため、パフォーマンスが重要なアプリケーションにも(一般的に)適していません。

原則として、パフォーマンス クリティカルなアプリケーションはケースごとに最適化されます (測定し、モジュール内で最悪のパフォーマンス問題を持つコードを選択し、最適化します)。

このケースごとのアプローチでは、コンパイラが vtbl をトラバースする必要があるため、コードが遅くなるケースはおそらくないでしょう。その場合、遅さは直接ではなくポインターを介して関数を呼び出すことが原因である可能性があります (つまり、基本クラスに余分なポインターを追加するのではなく、インライン化することで問題が解決されます)。

とにかく、最適化する具体的なケースがあるまで (そして、最悪の犯罪者は仮想関数呼び出しであることを測定します)、これはすべてアカデミックです。

編集

この方法が仮想呼び出しパラダイムの実行可能な代替品であるかどうか疑問に思っています。

これは一般的な解決策のように見えるため (どこにでも適用すると、パフォーマンスが向上するどころかパフォーマンスが低下します)、存在しない問題を解決します (通常、アプリケーションは仮想関数の呼び出しによって速度が低下することはありません)。

于 2013-06-27T13:58:08.960 に答える
1

仮想関数はテーブルを「トラバース」せず、ある場所からポインターを 1 回フェッチしてそのアドレスを呼び出すだけです。それは、関数へのポインターの手動実装があり、それを直接呼び出しの代わりに呼び出しに使用したかのようです。

したがって、あなたの作業は難読化にのみ有効であり、コンパイラが非仮想直接呼び出しを発行できる場合を妨害します。

メンバー関数へのポインターを使用することは、おそらく PTF よりもさらに悪いことです。同様のオフセット アクセスに同じ VMT 構造を使用する可能性が高く、固定ではなく可変アクセスのみを使用します。

于 2013-06-27T13:43:03.923 に答える
0

実際、一部のコンパイラは、通常の関数ポインタ自体に変換されるthunksを使用する場合があるため、基本的に、コンパイラは手動で実行しようとしていることを実行します (そして、おそらく人々を混乱させます)。

また、仮想関数テーブルへのポインターがあるため、仮想関数の空間の複雑さは O(1) (ポインターのみ) です。一方、クラス内に関数ポインターを格納すると、複雑さは O(N) になります (クラスには、「仮想」関数と同じ数のポインターが含まれるようになります)。多くの関数がある場合は、そのために料金を支払っています。オブジェクトをプリフェッチするときに、必要になる可能性が高い単一のポインターと最初のいくつかのメンバーではなく、すべてのポインターをキャッシュ ラインにロードします。それは無駄のように聞こえます。

一方、仮想関数テーブルは、1 つのタイプのすべてのオブジェクトに対して 1 つの場所に置かれ、コードがループ内でいくつかの短い仮想関数を呼び出している間、キャッシュからプッシュされることはおそらくありません (これはおそらく、仮想関数を使用する場合の問題です)。コストがボトルネックになる)。

分岐予測に関しては、場合によっては、オブジェクト型に対する単純なデシジョン ツリーと、特定の型ごとのインライン化された関数によって、優れたパフォーマンスが得られます (その後、ポインターの代わりに型情報を格納します)。これはすべてのタイプの問題に適用できるわけではなく、ほとんどの場合時期尚早な最適化です。

経験則として、言語構造はなじみがないように見えるため、心配する必要はありません。ボトルネックが実際にどこにあるかを測定して特定した後でのみ、心配して最適化してください。

于 2015-02-18T20:36:22.127 に答える