虚函数与性能-C + +

在我的类设计中,我广泛地使用抽象类和虚函数。我感觉虚函数会影响性能。这是真的吗?但是我认为这种性能差异并不明显,而且看起来我正在进行过早的优化。对吧?

76260 次浏览

一个很好的经验法则是:

除非你能证明,否则这不是性能问题。

虚拟函数的使用对性能的影响非常小,但不太可能影响应用程序的整体性能。寻找性能改进的更好地方是算法和 I/O。

成员函数指针和最快可能的 C + + 委托是一篇讨论虚函数(以及更多)的优秀文章。

当然。当计算机以100Mhz 运行时,这是一个问题,因为每个方法调用在调用之前都需要对 vtable 进行查找。但是今天。.在一个3Ghz 的 CPU,有1级缓存与更多的内存比我的第一台电脑有?完全没有。从主 RAM 分配内存要比所有功能都是虚拟的花费更多的时间。

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

我唯一会考虑考虑虚函数的性能影响的时候,就是它是否在模板代码中被大量使用和实例化,而这些代码最终贯穿了所有内容。即使那样,我也不会花太多精力在上面!

PS 想想其他“易于使用”的语言——它们所有的方法都是虚拟的,而且现在已经不再爬行了。

是的,您是对的,如果您对虚函数调用的成本感到好奇,您可能会对 这篇文章感兴趣。

使用虚函数带来的性能损失永远不会超过您在设计级别获得的优势。假设对虚函数的调用比对静态函数的直接调用效率低25% 。这是因为通过 VMT 存在一定程度的间接性。然而,与实际执行函数所花费的时间相比,调用所花费的时间通常是非常小的,因此总的性能成本将是微不足道的,特别是在当前硬件性能情况下。 此外,编译器有时可以进行优化,发现不需要虚拟调用,并将其编译为静态调用。因此,不要担心尽可能多地使用虚函数和抽象类。

当 Objective-C (其中所有方法都是虚拟的)是 iPhone 的主要语言,而该死的 爪哇咖啡是 Android 的主要语言时,我认为在我们的3GHz 双核塔上使用 C + + 虚拟函数是相当安全的。

我认为虚函数将成为性能问题的唯一方式是,如果在紧密循环中调用许多虚函数,并且 如果,也只有如果会导致出现页面错误或其他“沉重”内存操作。

不过就像其他人说的,在现实生活中这几乎不会成为你的问题。如果您认为有问题,请运行一个分析器,进行一些测试,并在尝试“反设计”代码以获得性能好处之前验证这是否真的是一个问题。

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

调用虚成员函数所需的时间比调用非虚成员函数所需的时钟周期多几个时钟周期,前提是函数调用语句总是调用相同版本的虚函数。如果版本发生变化,那么你将得到10-30个时钟周期的错误预测惩罚。虚函数调用的预测和错误预测规则与交换机语句的预测和错误预测规则相同。

除了执行时间之外,还有另一个性能标准。Vtable 也占用了内存空间,在某些情况下可以避免: ATL 使用编译时“ 模拟动态绑定模拟动态绑定”和 模板来获得“静态多态性”的效果,这有点难以解释; 你基本上把派生类作为参数传递给基类模板,所以在编译时基类“知道”它的派生类在每个实例中是什么。不允许在一个基类型集合中存储多个不同的派生类(即运行时多态性) ,但是从静态的角度来看,如果你想创建一个类 Y,它与之前存在的模板类 X 相同,这个类 X 有这种重写的钩子,你只需要重写你关心的方法,然后你就可以得到类 X 的基方法,而不需要 vtable。

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

参见 另一个问题

顺便说一下,这里的 我找到的一个帖子讨论了 CPU 时间性能方面的问题。

在性能非常关键的应用程序(如视频游戏)中,虚函数调用可能太慢。对于现代硬件,最大的性能问题是缓存丢失。如果数据不在缓存中,可能需要数百个周期才能使用。

当 CPU 获取新函数的第一条指令而它不在缓存中时,正常的函数调用会产生指令缓存丢失。

虚函数调用首先需要从对象加载 vtable 指针。这可能导致数据缓存丢失。然后它从 vtable 加载函数指针,这可能导致另一个数据缓存丢失。然后调用这个函数,这个函数会导致指令缓存像非虚函数一样丢失。

在许多情况下,两个额外的缓存丢失并不是一个问题,但是在性能关键代码的紧密循环中,它可以大大降低性能。

你的问题让我很好奇,所以我继续在我们使用的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) ,并运行一个循环,将它们相加(A.x = B.x + C.x)1000次。我使用定义为 inlinevirtual和常规函数调用的函数来运行它。结果如下:

  • 内联: 8毫秒(每次通话0.65纳秒)
  • 直拨: 68ms (每次通话5.53 ns)
  • 虚拟: 160ms (每次通话13ns)

因此,在这种情况下(所有内容都可以放在缓存中) ,虚函数调用的速度比内联调用慢20倍。但这到底意味着什么?每次通过循环都会导致 3 * 4 * 1024 = 12,288函数调用(1024个向量乘以4个组件乘以每次添加3个调用) ,因此这些时间表示 1000 * 12,288 = 12,288,000函数调用。虚拟循环的时间比直接循环长92毫秒,因此每次调用的额外开销是每个函数7 纳秒

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

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

我总是质疑自己这一点,特别是因为——很多年前——我也做过这样一个测试,比较标准成员方法调用和虚拟方法调用的时间,并对当时的结果感到非常愤怒,因为空虚方法调用比非虚拟方法调用慢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;
}

而且真的很惊讶,它-事实上-真的不再重要了。 虽然内联比非虚拟的更快,而且比虚拟的更快,但是它经常涉及到计算机的总体负载,不管你的缓存是否有必要的数据,而且你可能能够在缓存级别进行优化,我认为,这应该由编译器开发人员而不是应用程序开发人员来完成。

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

测试很好地说明了这一点,时差 ~ 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;
}

虚函数调用的影响很大程度上取决于具体情况。 如果函数内部只有很少的调用和大量的工作,那么它可以忽略不计。

或者,当它是一个重复使用多次的虚拟调用,同时做一些简单的操作-它可能真的很大。

我已经在这个项目上反复讨论了至少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;
}

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

我说“可能”是因为这取决于编译器、缓存等。