在类设计中,我广泛使用抽象类和虚函数。我感觉到虚函数会影响性能。这是真的?但是我认为这种性能差异并不明显,看起来我正在做过早的优化。对?
在类设计中,我广泛使用抽象类和虚函数。我感觉到虚函数会影响性能。这是真的?但是我认为这种性能差异并不明显,看起来我正在做过早的优化。对?
Answers:
一个好的经验法则是:
在您无法证明之前,这不是性能问题。
使用虚拟函数将对性能产生很小的影响,但是不太可能影响应用程序的整体性能。算法和I / O是寻求性能改进的更好地方。
讨论虚函数(以及更多内容)的出色文章是成员函数指针和最快的C ++委托。
您的问题使我感到好奇,所以我继续进行一些工作,在我们使用的3GHz顺序PowerPC CPU上进行了一些计时。我进行的测试是使用get / set函数制作一个简单的4d向量类
class TestVec
{
float x,y,z,w;
public:
float GetX() { return x; }
float SetX(float to) { return x=to; } // and so on for the other three
}
然后,我设置了三个数组,每个数组包含1024个向量(足够小以适合L1),并运行一个循环,将它们彼此相加(Ax = Bx + Cx)1000次。我定义为功能跑到这inline
,virtual
和普通函数调用。结果如下:
因此,在这种情况下(所有内容都适合缓存),虚拟函数调用的速度比内联调用慢20倍。但这到底是什么意思?遍历循环的每次3 * 4 * 1024 = 12,288
调用均引起函数调用(1024个矢量乘以四个分量乘以每个加法运算的三个调用),因此这些时间代表1000 * 12,288 = 12,288,000
函数调用。虚拟循环比直接循环花费92毫秒,因此每个调用的额外开销是每个函数7 纳秒。
由此我得出结论:是的,虚拟函数比直接函数要慢得多,而否,除非您打算每秒将它们调用一千万次,否则没关系。
另请参阅:生成的程序集的比较。
当Objective-C中(其中所有的方法都是虚拟的)是为iPhone和刻着的主要语言的Java是Android的主要语言,我认为这是相当安全的使用在我们的3 GHz的双核塔C ++虚函数。
在性能非常关键的应用程序(如视频游戏)中,虚拟函数调用可能太慢。对于现代硬件,最大的性能问题是高速缓存未命中。如果数据不在缓存中,则可能要经过数百个周期才能可用。
当CPU提取新功能的第一条指令且该指令不在高速缓存中时,正常的函数调用可能会生成指令高速缓存未命中。
首先,虚拟函数调用需要从对象中加载vtable指针。这可能会导致数据高速缓存未命中。然后,它从vtable加载函数指针,这可能导致另一个数据缓存未命中。然后,它调用该函数,这会导致指令缓存丢失,就像非虚函数一样。
在许多情况下,不必担心两个额外的高速缓存未命中,但是在性能关键代码紧密循环中,它可能会大大降低性能。
从Agner Fog的“ C ++优化软件”手册的第44页中:
假设函数调用语句始终调用相同版本的虚拟函数,则调用虚拟成员函数所需的时间比调用非虚拟成员函数所需的时间要多几个时钟周期。如果版本更改,则您将遭受10-30个时钟周期的误判惩罚。虚拟函数调用的预测和错误预测规则与switch语句的规则相同...
switch
。当然,具有完全任意的case
值。但是,如果所有case
s都是连续的,则编译器也许可以将其优化为跳转表(啊,这让我想起了Z80的美好时光),该表应该(为了更好的用语)应该是恒定时间的。并非我建议尝试用替换vfuncs switch
,这很荒谬。;)
绝对。当计算机以100Mhz的速度运行时,这是一个有问题的方法,因为每个方法调用都需要在调用vtable之前进行查找。但是,今天..在具有1级缓存且内存比我的第一台计算机更多的3Ghz CPU上吗?一点也不。与所有功能都是虚拟功能相比,从主RAM分配内存将花费更多时间。
就像过去,人们说结构化编程很慢,因为所有代码都被拆分成函数,每个函数都需要堆栈分配和函数调用!
我什至唯一想考虑一下虚拟函数对性能的影响的时候就是,如果虚拟函数在模板代码中被大量使用并实例化了,而该模板代码最终贯穿了所有内容。即使那样,我也不会花太多精力!
PS想到了其他“易于使用”的语言-它们的所有方法都是虚拟的,并且如今不再流行。
除了执行时间外,还有另一个性能标准。Vtable也占用内存空间,在某些情况下可以避免:ATL使用带有模板的编译时“ 模拟动态绑定 ”获得“静态多态性”的效果,这很难解释;您基本上将派生类作为参数传递给基类模板,因此在编译时,基类“知道”其在每个实例中的派生类。不允许您将多个不同的派生类存储在基本类型的集合中(即运行时多态性),但是从静态的角度来说,如果您想使类Y与已有的模板类X相同,则该类具有此类重写的钩子,您只需要重写您关心的方法,然后获得X类的基本方法,而不必使用vtable。
在具有较大内存占用量的类中,单个vtable指针的开销并不大,但是COM中的某些ATL类非常小,如果永远不会发生运行时多态情况,则值得节省vtable。
另请参阅其他SO问题。
我看到虚拟函数将成为性能问题的唯一方法是,如果在紧密循环内调用了许多虚拟函数,并且且仅当它们导致页面错误或其他“大量”内存操作发生时,才可以。
尽管就像其他人所说的那样,在现实生活中这几乎永远不会成为您的问题。如果您认为是这样,请运行探查器,进行一些测试,并在尝试对代码进行“非设计”以获得性能优势之前验证是否确实存在问题。
当类方法不是虚拟方法时,编译器通常进行内联。相反,当您使用带有虚函数的指向某个类的指针时,仅在运行时才知道真实地址。
测试可以很好地说明这一点,时差约为700%(!):
#include <time.h>
class Direct
{
public:
int Perform(int &ia) { return ++ia; }
};
class AbstrBase
{
public:
virtual int Perform(int &ia)=0;
};
class Derived: public AbstrBase
{
public:
virtual int Perform(int &ia) { return ++ia; }
};
int main(int argc, char* argv[])
{
Direct *pdir, dir;
pdir = &dir;
int ia=0;
double start = clock();
while( pdir->Perform(ia) );
double end = clock();
printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );
Derived drv;
AbstrBase *ab = &drv;
ia=0;
start = clock();
while( ab->Perform(ia) );
end = clock();
printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );
return 0;
}
虚拟函数调用的影响在很大程度上取决于情况。如果在函数内部调用很少且工作量很大,则可以忽略不计。
或者,当它是多次重复使用的虚拟呼叫时,同时执行一些简单的操作-可能会很大。
++ia
。所以呢?
在我的特定项目中,我至少来回了20次。尽管在代码重用性,清晰度,可维护性和可读性方面可以有一些巨大的收获,但另一方面,虚拟函数的确会影响性能。
在现代笔记本电脑/台式机/平板电脑上性能下降是否会显着...可能不会!但是,在嵌入式系统的某些情况下,性能下降可能是代码效率低下的驱动因素,尤其是在循环调用虚拟函数的情况下。
这是一篇过时的论文,它分析了嵌入式系统上下文中C / C ++的最佳实践:http : //www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf
总结一下:程序员应了解使用某种构造而非另一种构造的利弊。除非您是超级性能驱动者,否则您可能根本不在乎性能下降,应该使用C ++中所有精巧的OO工具来帮助使代码尽可能地可用。
以我的经验,最重要的是内联函数的能力。如果您有需要内联的性能/优化需求,则必须内联该函数,那么您就不能将其虚拟化,因为这样做会阻止这种情况。否则,您可能不会注意到差异。
要注意的一件事是:
boolean contains(A element) {
for (A current: this)
if (element.equals(current))
return true;
return false;
}
可能比这更快:
boolean contains(A element) {
for (A current: this)
if (current.equals(equals))
return true;
return false;
}
这是因为第一种方法仅调用一个函数,而第二种方法可能调用许多不同的函数。这适用于任何语言的任何虚拟功能。
我说“可能”是因为这取决于编译器,缓存等。
使用虚拟函数的性能损失永远不会超过您在设计级别获得的优势。假设对虚拟函数的调用比对静态函数的直接调用效率低25%。这是因为存在整个VMT的间接级别。但是,与实际执行函数相比,进行调用所需的时间通常很小,因此总性能成本可以忽略不计,尤其是在当前硬件性能的情况下。此外,编译器有时可以优化并看到不需要虚拟调用,并将其编译为静态调用。因此,不必担心会根据需要使用虚函数和抽象类。
The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.
说sometimes
,关键的区别是,不是never
。
我一直在问自己这个问题,特别是因为-几年前-我还进行了这样的测试,将标准成员方法调用与虚拟方法的调用时间进行了比较,并对当时的结果感到非常生气,因为有空的虚拟调用比非虚拟人慢8倍。
今天,我不得不决定是否在性能非常关键的应用程序中使用虚拟函数在我的缓冲区类中分配更多的内存,因此我用谷歌搜索(找到了你),最后再次进行了测试。
// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo> // typeid
#include <cstdio> // printf
#include <cstdlib> // atoll
#include <ctime> // clock_gettime
struct Virtual { virtual int call() { return 42; } };
struct Inline { inline int call() { return 42; } };
struct Normal { int call(); };
int Normal::call() { return 42; }
template<typename T>
void test(unsigned long long count) {
std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);
timespec t0, t1;
clock_gettime(CLOCK_REALTIME, &t0);
T test;
while (count--) test.call();
clock_gettime(CLOCK_REALTIME, &t1);
t1.tv_sec -= t0.tv_sec;
t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
? t1.tv_nsec - t0.tv_nsec
: 1000000000lu - t0.tv_nsec;
std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}
template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
test<T>(count);
test<Ua, Un...>(count);
}
int main(int argc, const char* argv[]) {
test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
return 0;
}
令我惊讶的是-实际上-不再重要了。内联比非虚拟更快,并且内联比虚拟更快,这通常是有意义的,但这通常涉及整个计算机的负载,无论您的缓存是否具有必需的数据,而且您可能可以进行优化我认为在缓存级别,这应该由编译器开发人员完成,而不是由应用程序开发人员完成。