当“虚拟”是一个相当大的开销时,有没有经验法则?

时间:2013-07-08 17:06:32

标签: c++ performance

我的问题基本上已在标题中完整陈述,但请让我详细说明。

问题: 也许值得重新措辞,virtual方法必须有多复杂/简单,使机制成为一个相当大的开销?这有什么经验法则吗?例如。如果需要10分钟,则使用I / O,复杂if语句,内存操作等,这不是问题。或者,如果您编写virtual get_r() { return sqrt( x*x + y*y); };并在循环中调用它,您将遇到麻烦。

我希望这个问题不是太笼统,因为我寻求一些一般但具体的技术答案。无论是难以辨认还是不可能,或者虚拟调用需要花费大量时间/周期资源,而数学需要这样,I / O就可以了。

也许一些技术人员知道要比较的一般数字或进行一些分析,并且可以分享一般性结论。令人尴尬的是,我不知道如何制作那些花哨的asm分析= /。

我还想提出一些理由,以及我的用例。

我认为,为了表现,人们在干旱期间避免使用像森林中的明火这样的虚拟现象,为了表现,我看到了很多问题,而且很多人都问他们“你是否绝对确定虚拟开销确实是一个问题在你的情况下?“。

在我最近的工作中,我遇到了一个问题,可以放在河的两边,我相信。

另外请记住,我不问如何改进界面的实现。我相信我知道该怎么做。我问是否有可能告诉你什么时候做,或者选择哪一个蝙蝠。

使用情况:

我运行了一些模拟。我有一个基本上提供运行环境的类。有一个基类,以及一个定义一些不同工作流的派生类。 Base将东西收集为通用逻辑并分配I / O源和接收器。衍生品通过实施RunEnv::run()或多或少来定义特定的工作流程。我认为这是一个有效的设计。现在让我们假设作为工作流主体的对象可以放在2D或3D平面中。在两种情况下,工作流都是通用的/可互换的,因此我们正在处理的对象可以具有通用接口,但是对于非常简单的方法,例如Object::get_r()。最重要的是,我们可以为环境定义一些stat logger。

最初我想提供一些代码片段,但最终只有5个类和2-4个方法,即code的墙。我可以根据要求发布它,但它会将问题延长到当前大小的两倍。

关键点是:RunEnv::run()是主循环。通常很长(5分钟-5小时)。它提供基本的时间检测,调用RunEnv::process_iteration()RunEnv::log_stats()。一切都是虚拟的。理由是。我可以推导出RunEnv,例如针对不同的停止条件重新设计run()。我可以重新设计process_iteration(),例如使用多线程,如果我必须处理一个对象池,以各种方式处理它们。此外,不同的工作流程还需要记录不同的统计信息。 RunEnv::log_stats()只是一个将已计算的有趣统计信息输出到std::ostream的调用。我猜测使用虚拟,并没有真正的影响。

现在让我们假设迭代通过计算对象到原点的距离来工作。所以我们有接口double Obj::get_r();Obj是2D和3D案例的实现。在两种情况下,getter都是一个简单的数学运算,有2-3次乘法和加法。

我还尝试了不同的内存处理。例如。有时坐标数据存储在私有变量中,有时存储在共享池中,因此即使get_x()也可以通过实现get_x(){return x;};get_x(){ return pool[my_num*dim+x_offset]; };变为虚拟变量。想象一下用get_r(){ sqrt(get_x()*get_x() + get_y()*get_y()) ;};计算一些东西。我怀疑这里的虚拟会破坏性能。

4 个答案:

答案 0 :(得分:8)

x86上的C ++中的虚方法调用产生类似于(单继承)的代码:

    mov ecx,[esp+4]
    mov eax,[ecx]       // pointer to vtable
    jmp [eax]           

如果没有虚拟,则与非虚拟成员函数相比,您将保留一条mov指令。因此,在单继承的情况下,性能损失可以忽略不计。

如果您有多个继承,或者更糟糕的是虚拟继承,虚拟调用可能会复杂得多。但这是类层次结构和体系结构的更多问题。

经验法则

如果方法的主体比单个mov指令慢很多倍(> 100x) - 只需使用virtual而不要打扰。否则 - 描述您的瓶颈并进行优化。

<强>更新

对于多个/虚拟继承案例,请查看此页面:http://www.lrdev.com/lr/c/virtual.html

答案 1 :(得分:8)

  

这有什么经验法则吗?

对于像这样的问题,最好的,最常见的经验法则是:

在优化之前衡量您的代码

尝试在不进行测量的情况下使代码运行良好,这是在不同地方优化的不必要复杂代码的可靠途径。

所以,在你有一些确凿的证据证明virtual是问题之前,不要担心虚函数的开销。如果您确实有这样的证据,那么您可以在这种情况下删除virtual。但是,更有可能的是,您会发现找到加速计算的方法,或者避免计算您不需要的计算方法,将会产生更大的性能提升。但同样,不要只是猜测 - 先测量。

答案 2 :(得分:3)

首先,当然,任何差异都取决于编译器, 架构等等。在某些机器上,区别 虚拟呼叫和非虚拟呼叫几乎不可测量, 至少在另一方面,它将(或将 - 我的经验) 这台机器相当古老)完全吹扫管道 (没有间接跳跃的分支预测)。

在大多数处理器上,虚拟功能的实际成本是内联能力的丧失, 由此导致其他优化可能性的丧失。换句话说,成本 实际上将取决于调用函数的上下文。

然而,更重要的是:虚拟功能和非虚拟功能 函数具有不同的语义。所以你不能选择:如果你 需要虚拟语义,你必须使用虚拟;如果你不这样做 需要虚拟语义,你不能使用虚拟。所以问题 真的没有出现。

答案 3 :(得分:1)

绝对最基本的建议,正如其他人所说的那样,您应该在特定的应用程序和环境中进行分析,是为了避免在紧密循环中virtual

请注意,如果您实际上需要多态行为,虚拟成员函数可能会比大多数替代方案更好。例外情况可能是您拥有多态但同质类型的集合(集合可以是任何多态类型,但它们都是相同的类型,无论它们碰巧是哪种类型)。然后,你可以更好地将多态行为移到循环之外。

使用经典的dumb bad-OO示例使用形状,你最好用:

// "fast" way
struct Shape {
  virtual void DrawAll(Collection) = 0;
};

struct Rectangle : public Shape {
  virtual void DrawAll(Collection collection) {
    for (const auto& rect : collection)
      do_rectangle_draw();
  }
};

struct Circle : public Shape {
  virtual void DrawAll(Collection collection) {
    for (const auto& circle : collection)
      do_circle_draw();
  }
};

比更天真的版本可能是这样的:

// "slow" way
struct Shape {
  virtual void DrawSelf() = 0;

  void DrawAll(Collection collection) {
    for (const auto& shape : collection)
      shape.DrawSelf(); // virtual invocation for every item in the collection!
  }
};

同样,这仅适用于集合中的同类型。如果您的Collection可以同时包含RectangleCircle,那么您将需要在迭代期间就每个实例区分使用哪种绘图方法。虚函数可能比函数指针或switch语句更快(但确定配置文件)。

上面代码的目标是将多态行为移出循环。这并不总是可行的,但是当它成功时,它通常会达到某种程度的性能胜利。对于大量对象(例如,粒子模拟器),性能差异可能非常明显。

如果一个函数在循环内没有被调用数千次,你可能不会注意到虚函数和非虚函数之间存在任何可测量的差异。但要对其进行测试以确定是否重要。