2013-01-14 38 views
5

这是我的问题。我有一个基类和一个派生类,它覆盖了基类中的一些方法。为简单起见考虑下面的例子:使用static_cast可以避免vtable开销吗?

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的,但不是很熟悉。这就是为什么我想知道这个问题的答案很简单的问题先:)

+2

什么是您的*显著开销定义*?您可能会惊讶于几百万次调用虚函数的开销。 –

+1

您是否真的证明vtable开销是您的代码的问题?如果是这样,你能证明做了相关的“如果(这个班)这样做是否更快”?我怀疑它不是,除非它使编译器内联函数,并且节省了大量的工作。 –

+0

我还没有证明它...这就是为什么我想首先创建这就避免了虚函数表的方法,这样我可以比较两个,并有那开销怎么显著是:) – linuxfever

回答

6

将这个解决方案避免了虚函数表的开销还是会仍然会受到影响?

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

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

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

+0

+1这是答案。如果您完全确定太多的CPU是大规模循环中的小型虚拟功能,并且无法正确修复,那么这是一项有效的技术。它正在转变为(...){(vtbl-> foo)();如果(a){for(...)A :: foo(); } else {for(...)B :: foo(); }'。不太可能造成太大的变化。 – doug65536

+0

@linuxfever如果它不是围绕1行左右虚拟函数的紧密循环,那么vtbl调用开销将可以忽略不计。有关紧密循环的好处在于对现代间接调用(比1997年的AMD K6)要好的分支预测工作。假设你的编译器不会产生太多的代码,并且传递参数,它会很快。 C++使用vtbls,因为它们是调用未知的最快方法。 – doug65536

+0

@linuxfever另一方面,如果编译器内联直接(非虚拟)调用,则编译器可能能够将大量常见子表达式提取出循环。 – doug65536

0

V表驻留在你的类。如果你有虚拟成员,他们将通过VTable访问。演员阵容不会影响VTable是否存在,也不会影响成员的访问方式。

+0

不正确。如果编译器在运行时知道变量的静态类型,它可以直接调用该函数而不是通过vtable。虚拟函数仅用于指针和引用。 –

+0

@MarkRansom无论编译器“知道”类型,所有对虚拟函数的调用都通过vtbl。全局优化器可以优化这种情况,但除非它正在执行配置文件引导的优化,否则它不太可能是积极的。通过绕开vtbl调用获得性能的关键是内联。除非编译器确定大多数调用是针对某种类型的,否则可能只是间接调用。 – doug65536

+0

@doug65536,看看生成的汇编程序对本地变量(而不是指针或引用)上的虚函数的调用,并告诉我你看到了什么。我会同意避免这个vtable可能不会买太多。 –

5

这是不是一个真正的答案,你的实际问题,但我很好奇“真正是调用虚函数调用VS常规类函数的开销”。为了使其“公平”,我创建了一个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 

类。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更有意义。

+0

+1尼斯分析,尽管您应该在“真实差异”中包含查找。如果你显示的两条线实际上是完全不同的话,虚拟实现将不会变慢! – us2012

+0

+1,还要注意,在实际代码中,函数可能不止一次加法,调用的差异将远低于函数的成本。 –

+0

这两行的区别在于一个使用间接寻址方法*(%rax),另一个使用直接地址'400850'。如果它比紧密循环中更少被调用,那么还有更多的工作要找到vtable等,但是你也有更多的其他代码来担心它对调用的影响。 –

0

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

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

相关问题