虚拟function和性能 – C ++

在我的课堂devise中,我广泛使用了抽象类和虚函数。 我有一种感觉,虚拟function会影响性能。 这是真的? 但我认为这种性能差异并不明显,看起来我正在做过早的优化。 对?

一个好的经验法则是:

直到你能certificate这不是一个性能问题。

虚拟function的使用对性能影响很小,但不太可能影响应用程序的整体性能。 在algorithm和I / O中,更好的寻找性能改进的地方是。

一篇关于虚拟函数(甚至更多)的优秀文章是成员函数指针和最快可能的C ++代表 。

你的问题让我很好奇,于是我继续在我们使用的3GHz有序PowerPC CPU上运行一些定时。 我跑的testing是用get / set函数做一个简单的4dvector类

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 } 

然后我build立了三个数组,每个数组包含1024个这样的vector(足够小以适应L1),并运行一个循环,将它们相加(Ax = Bx + Cx)1000次。 我用定义为inline函数, virtual函数和常规函数调用的函数来运行这个函数。 结果如下:

  • 内联:8毫秒(每个呼叫0.65ns)
  • 直接:68ms(每通话5.53ns)
  • 虚拟:160ms(每通话13ns)

因此,在这种情况下(一切都适合caching),虚拟函数调用比内联调用慢大约20倍。 但是这是什么意思呢? 每次通过循环时,都会产生3 * 4 * 1024 = 12,288函数调用(1024个向量乘以四个组件乘以每次调用三次调​​用),所以这些时间代表1000 * 12,288 = 12,288,000函数调用。 虚拟循环比直接循环长92毫秒,所以每个函数的额外开销是每个函数7 纳秒

由此我得出结论: 是的 ,虚拟函数比直接函数慢得多, ,除非你打算每秒钟调用它一千万次,否则没关系。

另见: 生成的程序集比较。

当Objective-C(所有方法都是虚拟的)是iPhone的主要语言,而且“ Java是Android的主要语言”时,我认为在我们的3 GHz双核心塔上使用C ++虚拟function是非常安全的。

在非常性能苛刻的应用程序(如video游戏)中,虚拟函数调用可能太慢。 使用现代硬件,最大的性能问题是caching未命中。 如果数据不在高速caching中,可能需要数百个周期才能使用。

当CPU获取新函数的第一条指令并且它不在caching中时,正常的函数调用会产生指令caching未命中。

虚函数调用首先需要从对象中加载vtable指针。 这可能会导致数据caching未命中。 然后它从vtable加载函数指针,这可能导致另一个数据caching未命中。 然后调用可能导致指令caching未命中的函数,如非虚函数。

在许多情况下,两个额外的高速caching未命中不是问题,但是在性能严重的代码中紧密循环会显着降低性能。

Agner Fog的“C ++优化软件”手册第44页:

调用虚拟成员函数所需的时间比调用非虚拟成员函数所用的时间多几个时钟周期,前提是函数调用语句始终调用相同版本的虚拟函数。 如果版本改变,那么你会得到10-30个时钟周期的预测失误。 虚拟函数调用的预测和误预测的规则与switch语句相同。

绝对。 当计算机在100Mhz下运行时,这是一个问题,因为每个方法调用之前都需要在vtable上查找。 但今天..在一个3Ghz的CPU有一级caching比我的第一台计算机有更多的内存了? 一点也不。 从主内存分配内存将花费更多的时间,如果你的所有function都是虚拟的。

就像过去那些人们说结构化编程很慢的旧时代一样,因为所有的代码都被分割成了函数,每个函数都需要堆栈分配和函数调用!

唯一一次我甚至想到考虑虚拟函数的性能影响,就是如果在模板化的代码中被大量使用和实例化,那么在所有的事情中都会结束。 即使那样,我也不会花太多的精力!

PS想到其他“易于使用”的语言 – 他们所有的方法都是虚拟的,现在它们不会爬行。

是的,你是对的,如果你对虚函数调用的成本感到好奇,你可能会觉得这个post很有趣。

除了执行时间还有另一个性能标准。 一个Vtable也占用内存空间,在某些情况下可以避免:ATL使用编译时“ 模拟dynamic绑定 ”和模板来获得“静态多态性”的效果,这是很难解释的; 您基本上将派生类作为parameter passing给基类模板,因此在编译时基类“知道”它的派生类在每个实例中是什么。 如果你想让一个类Y与一个已经存在的模板类X相同,不会让你在一个基types集合(即运行时多态性)中存储多个不同的派生类,但是从静态的angular度来说,这种重写的钩子,你只需要重写你关心的方法,然后你得到类X的基本方法,而不必有一个虚拟表。

在内存占用大的类中,单个vtable指针的代价并不是很大,但COM中的一些ATL类非常小,如果运行时多态情况永远不会发生,那么值得节省vtable。

另见另一个SO问题 。

顺便说一下, 我发现这篇文章讨论了CPU时间性能方面的问题。

当类方法不是虚拟的时候,编译器通常会进行内联。 相反,当你使用指向具有虚函数的某个类的指针时,真实的地址只有在运行时才知道。

这通过testing很好地说明了,时间差〜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; } 

虚拟函数调用的影响很大程度上取决于情况。 如果function内部没有多less电话和大量的工作 – 这可能是微不足道的。

或者,当这个虚拟的电话重复使用了很多次,在做一些简单的操作的时候 – 可能真的很大。

我可以看到虚拟函数将成为一个性能问题的唯一方法就是如果在严格的循环中调用了很多虚函数,并且当且仅当它们导致页面错误或其他“重”内存操作发生。

虽然像其他人一样,在现实生活中,你几乎不会成为一个问题。 如果你认为是这样,运行一个分析器,做一些testing,并validation这是否真的是一个问题,然后试图“取消签名”您的代码的性能优势。

我在这个特定的项目上至less进行过20次这样的事情。 尽pipe在代码重用,清晰性,可维护性和可读性方面可能会有一些很大的收益,但另一方面,虚拟function的性能命中仍然存在。

现代笔记本电脑/台式电脑/平板电脑的性能会受到影响…可能不会! 但是,在某些embedded式系统的情况下,性能的下降可能是代码效率低下的驱动因素,特别是在虚拟函数被循环调用的情况下。

下面是一些在embedded式系统上下文中描述C / C ++最佳实践的date报告: 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; } 

这是因为第一个方法只调用一个函数,而第二个方法可能调用了许多不同的函数。 这适用于任何语言的任何虚拟function。

我说“可能”,因为这取决于编译器,caching等

使用虚函数的性能损失永远不会超越您在devise级别获得的优势。 假设对一个虚拟函数的调用在直接调用一个静态函数时效率会降低25%。 这是因为在VMT之间存在一个间接的级别。 但是,与实际执行function所用的时间相比,拨打电话的时间通常非常短,因此总体性能成本将是可以忽略的,特别是在当前的硬件性能下。 此外,编译器有时可以优化并查看不需要虚拟调用,并将其编译为静态调用。 所以不要担心使用虚函数和抽象类,只要你需要。

我总是质疑自己,尤其是在几年前 – 我也做了一个比较标准成员方法调用和虚拟调用的时间的testing,并且真的对当时的结果感到愤怒,比非虚拟速度慢8倍。

今天,我必须决定是否使用虚拟function在缓冲区类中分配更多的内存,在一个非常关键的应用程序中,所以我search了(并find了你),最后再次做了testing。

 // 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; } 

真的很惊讶,事实上 – 根本就不重要了。 虽然内联比非虚内联更为合理,而且内联速度更快,但虚拟化速度更快,但是它总是会影响整个计算机的负载,无论您的caching是否具有必要的数据,尽pipe您可能能够优化在caching层面,我认为这应该由编译器开发人员完成,而不是由应用程序开发人员完成。