使用static_cast可以避免vtable开销吗?

时间:2013-01-14 23:00:16

标签: c++ vtable overhead static-cast

这是我的问题。我有一个基类和一个派生类,它覆盖了基类中的一些方法。为简单起见,请考虑以下示例:

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 个答案:

答案 0 :(得分:6)

  

此解决方案是否会避免vtable开销,还是会受到影响?

它仍将使用动态调度(是否会导致任何明显的开销是一个完全不同的问题)。您可以通过限定函数调用来禁用动态调度,如下所示:

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

虽然我甚至不想这样做。保留动态调度,然后测试应用程序的性能,进行一些分析,进行进一步的分析。再次描述以确保您了解分析器告诉您的内容。只有这样,才考虑再次进行一次更改和配置文件,以验证您的假设是否正确。

答案 1 :(得分:5)

这不是你实际问题的真正答案,但我很好奇“调用虚函数与​​调用常规类函数的真正开销是什么”。为了使它“公平”,我创建了一个实现非常简单的函数的classes.cpp,但它是在“main”之外编译的单独文件中。

classes.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

classes.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个时钟。

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

答案 2 :(得分:0)

VTable位于您的班级。如果您有虚拟成员,则可通过VTable访问它们。演员表不会影响VTable是否存在,也不会影响成员的访问方式。

答案 3 :(得分:0)

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

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