【问题标题】:Test for overhead of virtual functions测试虚函数的开销
【发布时间】:2011-03-09 13:54:17
【问题描述】:

我设置了一个(可能非常不科学的)小测试来确定单级单继承中虚函数的开销,我得到的结果在多态访问派生类或访问派生类时完全一样直接地。有点令人惊讶的是当任何函数被声明为虚拟时引入的计算时间的数量级(见下面的结果)。

这样声明成员函数时是否有这么多开销,为什么即使直接访问派生类仍然存在?

代码如下:

class base
{
public:
    virtual ~base() {}
    virtual uint func(uint i) = 0;
};

class derived : public base
{
public:
    ~derived() {}
    uint func(uint i) { return i * 2; }
};

uint j = 0;
ulong k = 0;
double l = 0;
ushort numIters = 10;
base* mybase = new derived;  // or derived* myderived = ...

for(ushort i = 0; i < numIters; i++)
{
  clock_t start2, finish2;
  start2 = clock();

  for (uint j = 0; j < 100000000; ++j)
        k += mybase->func(j);

  finish2 = clock();
  l += (double) (finish2 - start2);
  std::cout << "Total duration: " << (double) (finish2 - start2) << " ms." << std::endl;

}

std::cout << "Making sure the loop is not optimized to nothing: " << k << std::endl;
std::cout << "Average duration: " << l / numIters << " ms." << std::endl;

结果:

base* mybase = new derived; 的平均值约为 338 毫秒。

derived* myderived = new derived; 的平均值约为 338 毫秒。

消除继承和移除虚函数的平均时间约为 38 毫秒。

这几乎减少了 10 倍!所以基本上,如果任何函数被声明为虚拟,即使我不以多态方式使用它,开销也将始终相同?

谢谢。

【问题讨论】:

  • 看来你是在计算继承+虚函数的成本。您应该测试没有任何虚函数的派生类以及基类的实例化。
  • 为什么使用new ?实例化堆栈上的对象会更简单...
  • 在这种情况下绝对没有区别。如果通过指针访问,开销相同。

标签: c++ performance optimization virtual overhead


【解决方案1】:

“直接”访问它与“间接”访问它的工作相同。

当您在myderived 上调用函数时,存储在那里的指针可能指向从derived 派生的某个类的某个对象。编译器不能假设它真的是一个derived 对象,它可能是一个覆盖虚函数的进一步派生类的对象,因此需要像mybase 的情况一样进行虚函数调度。在这两种情况下,函数都会在调用之前在虚函数表中查找。

要以非多态方式调用函数,请不要使用指针:

derived myderived;
myderived.func(1); 

当你删除虚函数时,编译器可以内联函数调用,这样你基本上就得到了一个简单的循环:

for (uint j = 0; j < 100000000; ++j)
    k += i * 2;

这要快得多,因为您节省了 100000000 次函数调用的开销,而且编译器甚至可以进一步优化循环,如果其中有函数调用则不会。

还要注意,如果函数做一些实际工作,内联版本和虚函数调用之间的差异会小得多。在这个例子中,函数体几乎不需要任何时间,因此调用函数的成本超过了执行函数体的成本。

【讨论】:

  • 我明白了,谢谢。你说的很有道理。同意你关于功能太少的观点。这里的比例显然是有偏差的,但是知道函数调用开销是件好事。所以基本上在一天结束时,一旦你声明一个虚拟函数并通过指针使用它,你几乎已经支付了使用虚拟函数的全部代价。
  • s/编译器不能假设它真的是派生的/编译器不会假设它真的是派生的/。这是可能的,因为没有更多派生类(注意:确实需要整个程序优化)
  • 我试图删除指针,但它比它慢。
  • @YohaïBerreby 您需要实际记录您所做的事情,才能解释为什么它变慢了。它可能是各种各样的东西 - 但在最简单的层面上,如果做错了,每次迭代可能会产生一个对象副本/切片,而指针不会受到攻击。这将是代码中的一个问题,而不是支持/反对静态或动态调度。
【解决方案2】:

虚拟功能基本上没有成本。大多数真正的性能问题都是由不必要的密集调用树造成的,这些调用树做了一些你永远猜不到的事情。

我找到它们的方法是在调试器下多次暂停应用程序,并检查状态,包括调用堆栈。 Here's an example 使用该方法获得 43 倍的加速。

【讨论】:

  • 感谢 Mike,这是一个了不起的方法。
猜你喜欢
  • 2010-12-10
  • 1970-01-01
  • 1970-01-01
  • 2011-10-09
  • 1970-01-01
  • 2012-06-02
  • 2017-07-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多