【问题标题】:Cost of a virtual function in a tight loop紧密循环中虚拟功能的成本
【发布时间】:2011-07-06 15:37:49
【问题描述】:

我的游戏对象具有虚函数 Update()。有很多游戏对象(目前略多于 7000 个),并且循环调用所有这些对象的更新(除其他外)。我的同事建议我们应该完全删除虚拟功能。可以想象,这将需要大量的重构。

我见过this answer,但就我而言,分析意味着我必须更改很多代码。因此,在我考虑开始之前,我想我会在这里询问关于在这种情况下重构是否值得的意见。

请注意,我已经分析了循环的其他部分,并一直在尝试优化耗时最长的部分。我怀疑这种情况下的虚函数调用是我不应该担心的,但是在我分析之前我不能确定,并且在我更改代码之前我无法分析(这很多)。另请注意,一些更新函数非常小,而另一些则更大更复杂。

编辑:有多个答案可以提供很好的洞察力,因此将来偶然发现此问题的任何人都可以查看所有答案,而不仅仅是选定的答案。

【问题讨论】:

  • 从您提供的信息来看,重构似乎很困难或不可能。原因是有几种不同类型的 Update() 函数。通过重构,您将得到的只是虚函数调用将被 switch-case 或 if 语句替换,它们的性能并没有更好。
  • 居然有人问优化问题! :P ...像往常一样,在这些类型的问题上为-1,但不解释原因。无论如何,感谢所有回复的人。我所需要的只是另一种意见,我得到了。不知道为什么有些人一心想埋葬所有甚至有optimize字样的东西
  • 这里也一样。我正在开发自己的游戏引擎,目前在顶级机器上以大约 10fps 的速度运行。不知道在哪里看,但我不敢问。在我的引擎中,我使用了两个列表,一个用于所有对象(其数量超过系统中的活动对象 - 碰撞不会使对象处于活动状态),另一个用于活动对象。

标签: c++ optimization virtual-functions


【解决方案1】:

虚函数调用只会增加一个间接和难以预测的跳转。这意味着通常您会关闭一个管道刷新或每个虚拟功能大约 20 个周期。其中 7000 个大约是 140000 个周期,与您的平均更新函数相比应该可以忽略不计。如果不是,假设您的大多数更新函数只是空的,您可以考虑将可更新的对象放在一个单独的列表中。

删除虚拟功能只会导致你们中的某个人用相同但自行实现的系统替换它。这正是虚函数有意义的地方。

每个参考,140000 个周期大约是 50 微秒。这是假设一个 P4 有一个巨大的管道并且总是一个完整的管道刷新(你通常不会得到)。

【讨论】:

  • 非常好,这对我的决定肯定有帮助,谢谢 (+1)
【解决方案2】:

虽然它不是相同的代码,并且可能与您使用的编译器不同,但这里有一些来自相当老的基准测试(Joe Orost 的 bench++)的参考数据:

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006


Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005


Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006


Test Name:   F000008                         Class Name:  Style
CPU Time:        2.20  nanoseconds           plus or minus      0.110
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way virtual function class
 compare this test with F000006

这个特殊的结果来自使用 VC++ 9.0 (VS 2008) 的 64 位版本进行编译,但它与我从其他最近的编译器中看到的相当相似。最重要的是,虚拟函数比大多数明显的替代方法更快,并且 非常 接近与唯一击败它的速度相同的速度(事实上,两者相等是在测量范围内误差范围)。然而,这取决于所涉及的值是否密集——正如您在 F00007 中看到的那样,如果值是稀疏的,则 switch 语句生成的代码比虚函数调用慢。

底线:虚函数调用可能是错误的地方。重构的代码可能很容易运行得更慢,甚至充其量也可能不会获得足够的关注或关注。

【讨论】:

  • 哇,太好了,感谢您抽出宝贵时间来做这件事。它巩固了我不担心它的决定(+1)
  • +1 作为事后的想法:C++ 已经存在了几年,硬件开发人员已经开始构建可以高效运行它的 CPU 也就不足为奇了。 :-)
  • @Jerry:你能提供一个指向源的链接吗?我似乎无法获得 bench++ 的搜索结果
  • @Samaursa:我希望我可以,但是稍微看了一下,我似乎也找不到。我想我可以把它上传到某个地方。有什么好的地方可以推荐吗?
  • @Jerry: 可以发到 githug 或者 bitbucket 吗?
【解决方案3】:

如果您无法分析,请查看汇编代码以了解查找的真正成本。这可能是一个简单的间接跳转,几乎不需要任何成本。

如果你需要重构,这里有一个建议:创建大量知道如何调用新的非虚拟update() 方法的“UpdateXxx”类。将它们收集到一个数组中,然后对它们调用update()

但我猜你不会节省太多,尤其是只有 7K 个对象。

关于分析的注意事项:如果你不能使用分析器(让我想知道为什么不),对update() 的调用计时,并记录花费超过 100 毫秒的调用。时间并不昂贵,它可以让您快速找出哪些调用最昂贵。

【讨论】:

  • 这是一个非常好的建议。考虑到你和 dascandy 的回答,我现在会忘记这个,但在我的业余时间会试一试你的建议,看看会发生什么 (+1)
【解决方案4】:

您可以在此处找到另一个包含虚拟、内联和直接调用的测试 [在此处输入链接描述][1] Virtual functions and performance - C++

【讨论】:

    猜你喜欢
    • 2013-12-27
    • 1970-01-01
    • 2011-01-13
    • 1970-01-01
    • 2010-11-03
    • 2014-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多