Virtual functions and performance - C++
在我的类设计中,我广泛使用抽象类和虚拟函数。我觉得虚拟功能会影响性能。这是真的吗?但我认为这种性能差异并不明显,看起来我正在做过早的优化。对吗?
你的问题让我很好奇,所以我在3GHz上进行了一些计时,以便我们使用PowerPCCPU。我运行的测试是用get/set函数生成一个简单的4d向量类。
1 2 3 4 5 6 7 | 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),并运行了一个循环,将它们相互添加(a.x=b.x+c.x)1000次。我用定义为
- 内联:8ms(每次呼叫0.65ns)
- 直接:68ms(每次通话5.53ns)
- 虚拟:160ms(每次通话13ns)
因此,在这种情况下(所有东西都放在缓存中),虚拟函数调用比内联调用慢20倍。但这到底意味着什么?每次通过循环的跳闸都会准确地导致
由此我得出结论:是的,虚拟函数比直接函数慢得多,而且不是,除非您计划每秒调用一千万次,否则这并不重要。
另请参见:生成程序集的比较。
一个好的经验法则是:
It's not a performance problem until you can prove it.
使用虚拟函数对性能的影响很小,但不太可能影响应用程序的整体性能。寻找性能改进的更好地方是算法和I/O。
一篇关于虚拟函数(以及更多)的优秀文章是成员函数指针和最快的C++代表。
当Objto-C(其中所有方法都是虚拟的)是iPhone的主要语言,而Fravain’Java是Android的主要语言时,我认为在我们的3 GHz双核塔上使用C++虚拟功能是相当安全的。
在性能非常关键的应用程序(如视频游戏)中,虚拟函数调用可能太慢。对于现代硬件,最大的性能问题是缓存丢失。如果数据不在缓存中,可能需要数百个周期才能使用。
当CPU获取新函数的第一条指令而它不在缓存中时,正常的函数调用可能会生成指令缓存未命中。
虚拟函数调用首先需要从对象加载vtable指针。这可能导致数据缓存未命中。然后它从vtable加载函数指针,这可能导致另一个数据缓存未命中。然后它调用函数,这会导致指令缓存丢失,就像非虚拟函数一样。
在许多情况下,两个额外的缓存未命中不是一个问题,但是在性能关键的代码上,通过一个紧密的循环,它可以显著降低性能。
从Agner Fog的"C++优化软件"第44页开始:
The time it takes to call a virtual member function is a few clock cycles more than it takes to call a non-virtual member function, provided that the function call statement always calls the same version of the virtual function. If the version changes then you will get a misprediction penalty of 10 - 30 clock cycles. The rules for prediction and misprediction of virtual function calls is the same as for switch statements...
当然。当计算机以100MHz运行时,这是一个问题,因为在调用vtable之前,每个方法调用都需要查找它。但是今天…在3GHz的CPU上,第一级缓存的内存比我的第一台计算机多?一点也不。从主RAM分配内存将比所有函数都是虚拟的花费更多的时间。
就像以前人们说结构化编程很慢的时候,因为所有的代码都被分解成函数,每个函数都需要堆栈分配和函数调用!
唯一一次我会考虑考虑虚拟函数的性能影响的时候,是如果它被大量使用,并以模板化的代码进行实例化,最终在所有事情中结束。即使那样,我也不会花太多精力去做!
PS想到了其他"易于使用"的语言——他们所有的方法都是虚拟的,而且现在他们不爬行。
除了执行时间,还有另一个性能标准。vtable也占用了内存空间,在某些情况下可以避免:atl使用编译时的"模拟动态绑定"和模板来获得"静态多态性"的效果,这有点难以解释;您基本上将派生类作为参数传递给基类模板,因此在编译时,基类"知道"它的顺序Ived类在每个实例中。不会让您在一个基类型集合中存储多个不同的派生类(这是运行时多态性),但从静态意义上讲,如果您想使一个类Y与一个先前存在的模板类X相同,该模板类X具有用于这种重写的钩子,您只需要重写您关心的方法,然后获得BASe类的方法,无需vtable。
在内存占用较大的类中,单个vtable指针的成本不高,但是COM中的一些atl类非常小,如果永远不会发生运行时多态性的情况,那么值得节省vtable。
另见另一个问题。
顺便说一句,我发现这篇文章讨论了CPU时间性能方面的问题。
是的,你是对的,如果你对虚拟函数调用的成本感兴趣,你可能会发现这篇文章很有趣。
当类方法不是虚拟的时,编译器通常在内联中执行。相反,当使用指向带有虚函数的类的指针时,只有在运行时才知道实际地址。
这一点在测试中得到了很好的说明,时差约为700%(!):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | #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 ", (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 ", (end-start)/CLOCKS_PER_SEC, ia ); return 0; } |
虚拟函数调用的影响很大程度上取决于情况。如果在函数内部只有很少的调用和大量的工作,则可以忽略不计。
或者,当它是一个多次重复使用的虚拟调用时,同时执行一些简单的操作——它可能非常大。
我唯一能看到虚拟函数会成为性能问题的方法是,如果在一个紧密的循环中调用许多虚拟函数,并且如果并且仅当它们导致页面错误或其他"重"内存操作发生时。
尽管像其他人所说的,这在现实生活中永远不会成为你的问题。如果您认为是这样,请运行一个分析器,进行一些测试,并在尝试"取消签名"代码以获得性能优势之前验证这是否真的是一个问题。
在我的经验中,最主要的相关问题是内联函数的能力。如果您有性能/优化需求,要求一个函数需要内联,那么您就不能使该函数成为虚拟的,因为它会阻止这种情况发生。否则,您可能不会注意到差异。
在我的特定项目中,我已经反复讨论了至少20次。尽管在代码重用、清晰性、可维护性和可读性方面可以有很大的进步,但在另一方面,虚拟函数仍然存在性能冲击。
在现代笔记本电脑/台式机/平板电脑上,性能的冲击会很明显吗?大概不会吧!但是,在某些嵌入式系统的情况下,性能下降可能是导致代码效率低下的原因,特别是在循环中反复调用虚拟函数的情况下。
这里有一篇关于在嵌入式系统环境中C/C++的最佳实践的日期纸:HTTP://www-OpenStdOrg/JTC1/SC22/WG21/DOCS/ESCIO-BOSTONGO0130304Po.PDF
总而言之:由程序员来理解使用某个构造胜过另一个构造的优缺点。除非你是超级性能驱动的,你可能不关心性能命中,并且应该使用C++中所有整洁的OO东西来帮助你的代码尽可能的使用。
需要注意的一点是:
1 2 3 4 5 6 | boolean contains(A element) { for (A current: this) if (element.equals(current)) return true; return false; } |
可能比这快:
1 2 3 4 5 6 | boolean contains(A element) { for (A current: this) if (current.equals(equals)) return true; return false; } |
这是因为第一个方法只调用一个函数,而第二个方法可能调用许多不同的函数。这适用于任何语言的任何虚拟函数。
我说"可能",因为这取决于编译器、缓存等。
使用虚拟函数的性能损失永远不会超过您在设计级别所获得的优势。假设对虚拟函数的调用比直接调用静态函数效率低25%。这是因为通过vmt有一个间接级别。但是,与实际执行函数所花费的时间相比,进行调用所花费的时间通常非常小,因此总的性能成本将很低,特别是在当前硬件性能方面。此外,编译器有时可以优化并看到不需要虚拟调用,并将其编译为静态调用。所以不要担心使用虚拟函数和抽象类。
我一直在质疑自己这一点,尤其是几年前,我还做了这样一个测试,将标准成员方法调用的时间与虚拟方法调用的时间进行了比较,当时我对结果非常愤怒,因为空虚拟调用比非虚拟调用慢8倍。
今天,我必须决定是否使用一个虚拟函数在我的缓冲区类、一个性能非常关键的应用程序中分配更多的内存,所以我在谷歌上搜索(找到了你),最后再次进行了测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // 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 ... ", 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 ", 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; } |
我真的很惊讶它——事实上——真的不再重要了。虽然拥有比非虚拟更快的入口是有道理的,而且它们比虚拟更快,但通常涉及到计算机的总体负载,不管您的缓存是否有必要的数据,而且尽管您可能能够在缓存级别进行优化,我认为这应该由编译器开发人员完成,而不是由应用程序完成。开发者。