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要调用 , 百万次(在我的情况下,它会因为我的实际应用程序是一个模拟实验),我会得到一个我想避免的显着开销。

所以,我想知道使用 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,它实现了一个非常简单的功能,但它是一个在“main”之外编译的单独文件。

类.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 个时钟周期[包括将结果和其他所需处理相加],而虚拟调用每次增加 22 个时钟迭代,所以每次调用大约 1.5 个时钟。

我怀疑你会注意到,假设你在你的函数中做了一些比 return x + 1 更有意义的事情。

于 2013-01-14T23:49:28.263 回答
0

VTable 位于您的类中。如果您有虚拟成员,他们将通过 VTable 访问。强制转换不会影响 VTable 是否存在,也不会影响成员的访问方式。

于 2013-01-14T23:05:38.947 回答
0

如果您有一个多态数组,其中元素是多态的,但所有元素都具有相同的类型,您还可以将 vtable 外部化。这允许您查找该函数一次,然后直接在每个元素上调用它。在这种情况下,C++ 对您没有帮助,您必须手动完成。

如果您正在对事物进行微优化,这也很有用。我相信Boost的功能使用了类似的技术。它只需要 vtable 中的两个函数(调用和释放引用),但编译器生成的一个还包含 RTTI 和其他一些东西,这可以通过手动编码一个只有这两个函数指针的 vtable 来避免。

于 2013-01-15T06:56:21.987 回答