1

只是为了摆脱它...

Premature optimization is the root of all evil

Make use of OOP

etc.

我明白。只是寻找一些关于某些操作速度的建议,我可以将它们存储在我的灰质中以供将来参考。

假设您有一个动画课程。动画可以循环播放(反复播放)或不循环播放(播放一次),它可能有或没有唯一的帧时间,等等。假设有 3 个“非此即彼”属性。请注意,Animation 类的任何方法最多将检查其中之一(即,这不是 if-elseif 的巨大分支的情况)。

这里有一些选项。

1) 为上面给出的属性赋予布尔成员,并在播放动画时使用 if 语句检查它们以执行适当的操作。

  • 问题:每次播放动画时都会进行条件检查。

2)制作基础动画类,并派生其他动画类,如 LoopedAnimation 和 AnimationUniqueFrames 等。

  • 问题:鉴于您有类似vector<Animation>. 此外,为所有可能的组合创建一个单独的类似乎代码臃肿。

3)使用模板特化,并特化那些依赖于这些属性的函数。喜欢template<bool looped, bool uniqueFrameTimes> class Animation

  • 问题:这个问题是你不能只拥有一个vector<Animation>for 某物的动画。也可能臃肿。

我想知道这些选项中的每一个提供什么样的速度?我对第 1 和第 2 选项特别感兴趣,因为第 3 选项不允许遍历Animations 的一般容器。

简而言之,什么更快 - vtable fetch 或条件?

4

4 回答 4

4

(1)这些天生成的程序集的大小不再重要,但这就是它生成的(大约,假设 x86 上的 MSVC):

mov eax, [ecx+12]   ; 'this' pointer stored in ecx, eax is scratch
cmp eax, 0          ; test for 0 
jz  .somewhereElse  ; jump if the bool isn't set

优化编译器将在那里散布其他指令,使其对管道更加友好。无论如何,您的课程内容很可能在您的缓存中,如果不是,则无论如何都需要几个周期。所以,回想起来,这可能是几个周期,对于每帧最多调用几次的东西,这没什么。

(2) 这大约是每次调用 play() 方法时将生成的程序集:

mov  eax, [ebp+4]    ; pointer to your Animation* somewhere on the stack, eax is scratch
mov  eax, [eax+12]   ; dereference the vtable
call eax             ; call it

然后,您将在专门的 play() 函数中有一些重复的代码或另一个函数调用,因为肯定会有一些常见的东西,因此会产生一些开销(在代码大小和/或执行速度方面)。所以,这肯定会更慢。

此外,这使得加载通用动画变得更加困难。你的图形部门不会高兴的。

(3) 为了有效地使用它,无论如何,您最终都会为您的模板版本创建一个基类,使用虚函数(在这种情况下,请参阅 (2)),或者您将通过检查类型的位置来手动完成你把你的动画叫做东西,在这种情况下也见(2)。

这也使得加载通用动画变得更加困难。你的图形部门会更不高兴。

(4) 你需要担心的不是对每帧最多执行几次的微小事情进行一些微优化。通过阅读您的帖子,我实际上发现了另一个通常被忽视的问题。您提到的是 std::vector<Animation>。没有什么反对 STL,但那是糟糕的巫术。在您的应用程序运行的整个过程中,单个内存分配将花费您比 play() 或 update() 方法中的所有布尔检查更多的周期。将动画放入和取出 std::vectors (特别是如果您放入实例而不是指向实例的指针(智能或愚蠢))将花费您更多。

您需要查看不同的地方进行优化。这是一个如此荒谬的微优化,它不会给您带来任何好处,只会让您更难概括并让您的图形部门满意。然而,重要的是担心内存分配,然后,当您完成对该部分的编程时,启动分析器并查看热点在哪里。

如果保留动画实际上成为瓶颈,那么您可能想要查看 std::vector (尽管它很好)。你看过,比如说,一个侵入式链表吗?这实际上比担心这个更有好处。

于 2010-05-13T23:43:43.037 回答
3

(为简洁而编辑。)

编译器、CPU 和操作系统都可以改变答案,这里:

  • CPU:指令/数据缓存大小、架构和行为,尤其是任何智能预取
  • CPU:分支预测和推测执行行为
  • CPU:错误预测分支的惩罚
  • 编译器和 CPU:条件执行指令的可用性和相对成本(有助于仅涵盖少数指令的分支案例)
  • 编译器或链接器:可能会转换您的代码并删除分支的优化

简而言之,正如 Blindy 在评论中所说:测试它。=)

如果您正在为现代桌面操作系统或操作系统编写代码,请寻求分析工具(valgrind、shark、codeanalyst、vtune 等)的帮助——它可能会为您提供您甚至不知道可以寻找的详细信息,例如缓存未命中、分支错误预测等。

即使您没有找到一个很好的答案,您也会从应用该工具中学到一些东西。我经常发现查看反汇编也很有启发性(请参阅此线程中的其他一些答案)。

一些稍微投机的笔记:

  • vtable 往往会导致加载(this+0)、偏移、第二次加载,然后在寄存器的内容上进行分支。您可以在其他一些答案中看到这一点。我熟悉的大多数 CPU 在从寄存器预测分支方面都很糟糕。
  • 该布尔值可能靠近您正在使用的其他数据,因此可能已经被缓存。分支目标也可能是固定的,因此对预测和/或推测执行更加友好。
  • 在某些处理器上(现在很少见),加载 bool 比加载 int 成本更高。
  • 在我使用的 ARM 处理器上,我们偶尔会将 vtable 塞入处理器内核的“紧密耦合内存”中。大大减少了间接加载时间——就好像 vtable 总是在缓存中或更好。

正如您所提到的,通常的规则适用:首先做符合要求并且灵活/可维护/可读的事情,然后进行优化。

延伸阅读/其他模式追求:

“面向数据的设计”和“基于组件的实体”范式对于将游戏、多媒体引擎和其他对性能有高于平均水平的需求但仍希望保留代码有些组织。YMMV,当然。=)

于 2010-05-13T23:58:09.537 回答
2

Vtable 非常非常快。简单的条件也是如此。它们转换为单个数字的 CPU 指令。担心这种性能会让你陷入编译器优化的浑水,你根本不了解编译器在做什么。很有可能,程序中非常细微的变化可以胜过 if 语句和 vtable 之间的细微差别。

前段时间我做了一个小测试,测试 RTTI 多调度和 vtable 之间的差异。在发布模式下,完成超过 200 万次迭代的三个对象(两个 vtable 调用)之间的调度需要 62 毫秒。那是根本不值得担心的方式。

于 2010-05-13T23:44:28.807 回答
0

谁说#3 不可能有一个通用的动画容器?有几种方法可以使用。它们都归结为最终进行多态调用,但选项就在那里。考虑一下:

std::vector<boost::any> generic_container;
function(generic_container[0]);

void function(boost::any & a)
{
  my_operation::execute(a.type().name(), a);
}

my_operation 只需要有一种按类型名称注册和过滤操作的方法。它搜索一个对 a 表示的任何东西进行操作的函子,并使用它。仿函数然后 any_casts 到适当的时间并执行特定于类型的操作。

或者使用访问者框架。以上是一种变体,但水平太笼统,无法真正合格。

还有更多可能的方法。您可以存储一个隐藏细节并在激活时执行正确视图选项的类型,而不是存储动画。一个 virtual 被调用,但它专门用于切换对彼此执行更复杂操作的具体类型。

换句话说,您的问题没有一般性的答案。根据您的需要,您可以达到各种复杂程度,以使几乎整个程序的编译时多态,而不是运行时。

于 2010-05-13T23:52:18.553 回答