在我的类设计中,我广泛使用抽象类和虚函数。我有一种感觉,虚拟功能会影响性能。这是真的?但我认为这种性能差异并不明显,看起来我正在做过早的优化。正确的?
你的问题让我很好奇,所以我继续在我们使用的 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
的函数和常规函数调用来运行它。结果如下:
内联:8ms(每次调用 0.65ns)
直接:68ms(每次调用 5.53ns)
虚拟:160ms(每次调用 13ns)
因此,在这种情况下(一切都适合缓存),虚函数调用比内联调用慢 20 倍左右。但这究竟意味着什么?循环中的每次行程恰好导致 3 * 4 * 1024 = 12,288
次函数调用(1024 个向量乘以 4 个分量乘以每次添加的 3 次调用),因此这些时间代表 1000 * 12,288 = 12,288,000
次函数调用。虚拟循环比直接循环多花 92 毫秒,因此每次调用的额外开销是每个函数 7 纳秒。
由此我得出结论:是的,虚函数比直接函数慢得多,而且不,除非您计划每秒调用它们一千万次,否则没关系。
另请参阅:comparison of the generated assembly.
一个好的经验法则是:
在您能够证明之前,这不是性能问题。
使用虚函数对性能的影响很小,但不太可能影响应用程序的整体性能。寻找性能改进的更好地方是算法和 I/O。
Member Function Pointers and the Fastest Possible C++ Delegates 是一篇讨论虚函数(及更多内容)的优秀文章。
当 Objective-C(所有方法都是虚拟的)是 iPhone 的主要语言,而该死的 Java 是 Android 的主要语言时,我认为在我们的 3 GHz 双核塔上使用 C++ 虚拟函数是非常安全的。
在性能非常关键的应用程序(如视频游戏)中,虚拟函数调用可能太慢。对于现代硬件,最大的性能问题是缓存未命中。如果数据不在缓存中,则可能需要数百个周期才能使用。
当 CPU 获取新函数的第一条指令并且它不在缓存中时,正常的函数调用会产生指令缓存未命中。
虚函数调用首先需要从对象加载 vtable 指针。这可能导致数据缓存未命中。然后它从 vtable 加载函数指针,这可能导致另一个数据缓存未命中。然后它调用可能导致指令缓存未命中的函数,就像非虚拟函数一样。
在许多情况下,两个额外的缓存未命中不是问题,但在性能关键代码的紧密循环中,它会显着降低性能。
来自 Agner Fog's "Optimizing Software in C++" manual 第 44 页:
调用一个虚成员函数所花费的时间比调用一个非虚成员函数所花费的时间要多几个时钟周期,前提是函数调用语句总是调用相同版本的虚函数。如果版本发生变化,那么您将获得 10 - 30 个时钟周期的误预测惩罚。虚函数调用的预测和错误预测规则与switch语句相同......
switch
是否总是正确。当然,使用完全任意的 case
值。但是如果所有的 case
都是连续的,编译器可能能够将其优化为一个跳转表(啊,这让我想起了 Z80 的美好时光),它应该是(为了更好的术语)恒定 -时间。 不是,我建议尝试用 switch
替换 vfunc,这很可笑。 ;)
rules for prediction and misprediction of virtual function calls is the same as for switch statements
的陈述在某种意义上也是正确的,假设 vtable 被实现为 switch-case,那么有两种可能性:1 )如果案例是连续的,它会被优化为跳转表(如你所说),2)它不能优化为跳转表,因为案例不连续,因此 will get a misprediction penalty of 10 - 30 clock cycles
如 Anger 所述。
绝对地。当计算机以 100Mhz 运行时,这是一个问题,因为每个方法调用都需要在调用之前查找 vtable。但是今天.. 在一个 3Ghz CPU 上,它具有比我的第一台计算机更多的内存的一级缓存?一点也不。与所有功能都是虚拟的相比,从主 RAM 分配内存将花费更多时间。
就像过去人们说结构化编程很慢,因为所有代码都被拆分成函数,每个函数都需要堆栈分配和函数调用!
唯一一次我什至会考虑考虑虚拟函数对性能的影响,是它是否被大量使用并在模板代码中实例化,最终贯穿所有内容。即便如此,我也不会在这上面花太多力气!
PS 想想其他“易于使用”的语言——它们所有的方法都是虚拟的,而且它们现在不会爬行。
除了执行时间之外,还有另一个性能标准。 Vtable 也会占用内存空间,在某些情况下可以避免:ATL 使用编译时“simulated dynamic binding”和 templates 来获得“静态多态”的效果,这有点难以解释;您基本上将派生类作为参数传递给基类模板,因此在编译时基类“知道”每个实例中的派生类是什么。不会让您将多个不同的派生类存储在基类型的集合中(即运行时多态性),但从静态意义上讲,如果您想创建一个与预先存在的模板类 X 相同的类 Y,它具有对于这种覆盖的钩子,你只需要覆盖你关心的方法,然后你就可以得到类 X 的基方法,而不必有一个 vtable。
在具有大内存占用的类中,单个 vtable 指针的成本并不多,但 COM 中的一些 ATL 类非常小,如果永远不会发生运行时多态情况,那么节省 vtable 是值得的。
顺便说一下,这里的 a posting I found 讨论了 CPU 时间性能方面。
我可以看到虚拟函数成为性能问题的唯一方法是,如果在紧密循环中调用许多虚拟函数,并且当且仅当它们导致页面错误或其他“重”内存操作发生时。
尽管就像其他人所说的那样,在现实生活中这对你来说几乎永远不会成为问题。如果您认为是这样,请运行分析器,进行一些测试,并验证这是否真的是一个问题,然后再尝试“取消设计”您的代码以获得性能优势。
当类方法不是虚拟的时,编译器通常会进行内联。相反,当您使用指向具有虚函数的某个类的指针时,只有在运行时才能知道真实地址。
测试很好地说明了这一点,时间差 ~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(element))
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;
}
并且真的很惊讶它 - 事实上 - 真的不再重要了。虽然内联比非虚拟更快是有意义的,而且它们比虚拟更快,但它通常涉及到整个计算机的负载,无论你的缓存是否有必要的数据,虽然你可能能够优化在缓存级别,我认为这应该由编译器开发人员完成,而不是由应用程序开发人员完成。