6

これが私の問題です。基本クラスと、基本クラスのいくつかのメソッドをオーバーライドする派生クラスがあります。簡単にするために、次の例を考えてみましょう。

struct base
{
  virtual void fn()
  {/*base definition here*/}
};

struct derived : base 
{
  void fn()
  {/*derived definition here*/}
};

私の実際のプログラムでは、これらのクラスは引数として他のクラスに渡され、他のメソッドで呼び出されますが、簡単にするために、基本クラスまたは派生クラスのいずれかを引数として取る単純な関数を作成してみましょう。私は単に書くことができます

void call_fn(base& obj)
{obj.fn();}

適切な関数への呼び出しは、仮想関数により実行時に解決されます。

call_fnただし、が 100 万回呼び出されると (私の場合、実際のアプリケーションはシミュレーション実験であるため、そうなるでしょう)、回避したいかなりのオーバーヘッドが発生するのではないかと心配しています。

それで、 static_cast を使用して実際に問題に対処できるかどうか疑問に思っていました。多分このようなもの:

template <typename T>
void call_fn(base& obj)
{(static_cast<T*>(&obj))->fn();}

この場合、関数呼び出しはcall_fn<base>(obj)、基本メソッドcall_fn<derived>(obj)の呼び出しまたは派生メソッドの呼び出しとして行われます。

このソリューションは vtable のオーバーヘッドを回避しますか、それとも影響を受けますか? 返信ありがとうございます。

ところで、CRTP は知っていますが、あまり詳しくありません。そのため、最初にこの単純な質問に対する答えを知りたいのです:)

4

4 に答える 4

7

このソリューションは vtable のオーバーヘッドを回避しますか、それとも影響を受けますか?

引き続き動的ディスパッチを使用します (顕著なオーバーヘッドが発生するかどうかは、まったく別の問題です)。次のように関数呼び出しを修飾することで、動的ディスパッチを無効にすることができます。

static_cast<T&>(obj).T::fn();

私はそうしようとさえしませんが。動的ディスパッチを終了してから、アプリケーションのパフォーマンスをテストし、いくつかのプロファイリングを行い、さらにプロファイリングを行います。もう一度プロファイリングして、プロファイラーが何を伝えているかを確実に理解してください。その場合にのみ、単一の変更とプロファイルを再度作成して、仮定が正しいかどうかを確認することを検討してください。

于 2013-01-14T23:06:21.433 に答える
6

これはあなたの実際の質問に対する答えではありませんが、「仮想関数の呼び出しと通常のクラス関数の呼び出しのオーバーヘッドは実際にはどうなるか」について興味がありました。「公平」にするために、非常に単純な関数を実装する classes.cpp を作成しましたが、それは「メイン」の外でコンパイルされる別のファイルです。

クラス.h:

#ifndef CLASSES_H
#define CLASSES_H

class base
{
    virtual int vfunc(int x) = 0;
};

class vclass : public base
{
public:
    int vfunc(int x);
};


class nvclass
{
public:
    int nvfunc(int x);
};


nvclass *nvfactory();
vclass* vfactory();


#endif

クラス.cpp:

#include "classes.h"

int vclass:: vfunc(int x)
{
    return x+1;
}


int nvclass::nvfunc(int x)
{
    return x+1;
}

nvclass *nvfactory()
{
    return new nvclass;
}

vclass* vfactory()
{
    return new vclass;
}

これは次から呼び出されます。

#include <cstdio>
#include <cstdlib>
#include "classes.h"

#if 0
#define ASSERT(x) do { if(!(x)) { assert_fail( __FILE__, __LINE__, #x); } } while(0)
static void assert_fail(const char* file, int line, const char *cond)
{
    fprintf(stderr, "ASSERT failed at %s:%d condition: %s \n",  file, line, cond); 
    exit(1);
}
#else
#define ASSERT(x) (void)(x)
#endif

#define SIZE 10000000

static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


void print_avg(const char *str, const int *diff, int size)
{
    int i;
    long sum = 0;
    for(i = 0; i < size; i++)
    {
    int t = diff[i];
    sum += t;
    }

    printf("%s average =%f clocks\n", str, (double)sum / size);
}


int diff[SIZE]; 

int main()
{
    unsigned long long a, b;
    int i;
    int sum = 0;
    int x;

    vclass *v = vfactory();
    nvclass *nv = nvfactory();


    for(i = 0; i < SIZE; i++)
    {
    a = rdtsc();

    x = 16;
    sum+=x;
    b = rdtsc();

    diff[i] = (int)(b - a);
    }

    print_avg("Emtpy", diff, SIZE);


    for(i = 0; i < SIZE; i++)
    {
    a = rdtsc();

    x = 0;
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    ASSERT(x == 4); 
    sum+=x;
    b = rdtsc();

    diff[i] = (int)(b - a);
    }

    print_avg("Virtual", diff, SIZE);

    for(i = 0; i < SIZE; i++)
    {
    a = rdtsc();
    x = 0;
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    ASSERT(x == 4);     
    sum+=x;
    b = rdtsc();
    diff[i] = (int)(b - a);
    }
    print_avg("no virtual", diff, SIZE);

    printf("sum=%d\n", sum);

    delete v;
    delete nv;

    return 0;
}

コードの本当の違いはこれです: 仮想呼び出し:

40066b: ff 10                   callq  *(%rax)

非仮想呼び出し:

4006d3: e8 78 01 00 00          callq  400850 <_ZN7nvclass6nvfuncEi>

そして結果:

Emtpy average =78.686081 clocks
Virtual average =144.732567 clocks
no virtual average =122.781466 clocks
sum=480000000

これはループあたり 16 回の呼び出しのオーバーヘッドであるため、関数を呼び出す場合と関数を呼び出さない場合の違いは、反復ごとに約 5 クロック サイクル (結果の合計と必要なその他の処理を含む) であり、仮想呼び出しでは 1 回あたり 22 クロックが追加されます。繰り返しなので、呼び出しごとに約 1.5 クロックです。

関数で return x + 1 よりも少し意味のあることを行うと仮定すると、気付くとは思えません。

于 2013-01-14T23:49:28.263 に答える
0

要素が多態性であるがすべての要素が同じ型である多態性配列がある場合は、vtable を外部化することもできます。これにより、関数を一度検索してから、各要素で直接呼び出すことができます。その場合、C++ は役に立ちませんが、手動で行う必要があります。

これは、物事をマイクロ最適化している場合にも役立ちます。Boost の機能も同様の手法を使用していると思います。vtable には 2 つの関数 (call と release 参照) しか必要ありませんが、コンパイラによって生成された関数には RTTI やその他のものも含まれます。これは、これら 2 つの関数ポインターのみを持つ vtable をハンドコーディングすることで回避できます。

于 2013-01-15T06:56:21.987 に答える
0

VTable はクラスに存在します。仮想メンバーがある場合、それらは VTable を介してアクセスされます。キャストは、VTable が存在するかどうかにも、メンバーへのアクセス方法にも影響しません。

于 2013-01-14T23:05:38.947 に答える