7

我的问题基本上完全在标题中说明,但让我详细说明。

问题: 也许值得改写一下,该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(). 最重要的是,让我们为环境定义一些统计记录器。

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

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

现在假设迭代通过计算对象到原点的距离来工作。所以我们有 as interface 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

4 回答 4

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

于 2013-07-08T17:12:07.467 回答
8

这有什么经验法则吗?

对于像这样的问题,最好、最一般的经验法则是:

在优化之前测量你的代码

试图让你的代码在不测量的情况下表现良好是通往不必要的复杂代码的可靠途径,这些代码在所有错误的地方都进行了优化。

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

于 2013-07-08T17:16:19.750 回答
3

首先,当然,任何差异都取决于编译器、体系结构等。在某些机器上,虚拟调用和非虚拟调用之间的差异几乎无法测量,至少在另一台机器上,它会(或将---我对这台机器的经验相当古老)完全清除管道(间接跳转没有分支预测)。

在大多数处理器上,虚函数的真正成本是内联能力的损失,从而导致其他优化可能性的损失。换句话说,成本实际上取决于调用函数的上下文。

然而,更重要的是:虚函数和非虚函数具有不同的语义。所以不能选择:如果需要虚拟语义,就必须使用virtual;如果不需要虚拟语义,则不能使用虚拟。所以这个问题真的没有提出来。

于 2013-07-08T17:24:04.913 回答
1

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

请注意,如果您确实需要多态行为,则虚拟成员函数的行为可能会比大多数替代方法更好。当您有一个多态但同质类型的集合时可能是例外(该集合可以是任何多态类型,但它们都是相同的类型,无论它们碰巧是哪种类型)。然后,您最好将多态行为移到循环之外。

以使用形状的经典愚蠢 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可以同时包含Rectangles 和Circles,那么您将需要在迭代期间区分每个实例以使用哪种绘图方法。虚函数可能比函数指针或 switch 语句更快(但可以肯定的是配置文件)。

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

如果一个函数在循环中没有被调用数千次,您可能不会注意到虚拟函数和非虚拟函数之间有任何可测量的差异。但是对其进行分析以进行测试并确定您是否认为这很重要。

于 2013-07-08T19:04:58.770 回答