这是我的问题。我有一个基类和一个派生类,它覆盖了基类中的一些方法。为简单起见,请考虑以下示例:
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但不熟悉它。这就是为什么我想首先知道这个简单问题的答案:)
答案 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来避免。