2010-05-13 74 views
1

只是把它弄出来的方式...速度比较 - 模板专业化与虚拟功能与if语句

Premature optimization is the root of all evil

Make use of OOP

etc.

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

假设你有一个动画类。动画可以循环播放(循环播放)或不循环(播放一次),它可以具有唯一的帧时间等等。假设有3个这样的“任一”或“属性”。 请注意,Animation类的任何方法至多会检查其中的一个(即,这不是if-elseif的巨大分支的情况)。

这里有一些选择。

1)给它一个上面给出的属性布尔成员,并使用if语句来打去执行相应的操作动画时检查对付他们。

  • 问题:有条件地检查每次播放动画。

2)做一个基础的动画类,并从中获得其他动画类,如LoopedAnimation和AnimationUniqueFrames等

  • 问题:在每次调用的V表检查,以播放动画因为你碰到这样的一个vector<Animation>。另外,为所有可能的组合创建一个单独的类看起来代码笨拙。

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

  • 问题:与此问题是,你不能只有一个vector<Animation>的东西的动画。也可能会臃肿。

我想知道什么样的速度每个选项提供?我对第一和第二选项特别感兴趣,因为第三个选项不允许我们遍历一个通用容器Animation s。

总之,什么是快 - 一个虚函数表读取或条件?

+4

尝试自己测试一下。 – Blindy 2010-05-13 23:22:48

+1

这些属性的实际值是在运行时还是在编译时确定的?例如,如果您要加载用户指定的文件,那么属性在运行时确定,当然。如果动画是预先确定的,那么这些值在编译时已知。那么,你在处理哪种情况? – AnT 2010-05-13 23:41:40

回答

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 

然后,你就会有一些重复的代码或你的专门的打内线另一个函数调用()函数,因为它会定义一些常见的东西,所以会产生一些开销(代码大小和/或执行速度)。所以,这个速度肯定比较慢。

此外,这使加载通用动画更加困难。你的图形部门不会很高兴。 (3)为了有效地使用它,你最终会为你的模板化版本创建一个基类,使用虚函数(在这种情况下,请参阅(2)),或者你可以通过检查类型在你称之为动画的地方,在这种情况下,请参阅(2)。

这也使加载泛型动画变得更加困难。你的图形部门会更加不快乐。 (4)你需要担心的不是对于一个框架中几次完成的微小事情的微观优化。从阅读你的文章,我确定了另一个常被忽视的问题。你提到std :: vector <动画>。没有什么反对STL,但那是糟糕的巫术。一个单独的内存分配会花费比循环播放()或更新()方法中的所有布尔检查更多的周期,这可能是整个应用程序运行的时间。将动画放入和退出std :: vectors(特别是如果您将实例放入实例而不是指向实例的指针(智能或非哑))将会花费更多。

你需要看看不同的地方进行优化。这是一种非常荒谬的微观优化,除了使它更难概括并使你的图形部门感到高兴之外,它不会带来任何好处。但是,重要的是担心内存分配问题,然后,当您完成对该部分的编程后,启动一个分析器并查找热点位置。

如果让你的动画实际上成为一个瓶颈,std :: vector(很好,因为它是)你可能想看的地方。你有没有看过一个侵入式链表?这实际上会比这个担心更有益。

+0

只要您提前预留(),根据我的经验,矢量都可以。 – leander 2010-05-13 23:47:35

+0

确实如此,但假设你事先知道你在那里放了多少东西。尽管如此,你很少需要随机访问这样的东西,因此侵入式版本仍然(有点)更好,因为你不需要为这些指针分配缓冲区 - 反正分配的对象已经有了。 – arke 2010-05-14 00:03:30

+0

噢,还有一个+1会让人烦心出去拆解它=)我喜欢看编译器做什么,它几乎总是很有启发性。 – leander 2010-05-14 00:19:53

2

Vtable非常快。那么简单的条件。它们转换为CPU指令的单个数字。担心这种性能会让你陷入编译器优化的晦涩境地,在那里你完全不了解编译器在做什么。很有可能,程序中非常微妙的变化可能会超过if语句和vtable之间的微小差异。

我做了一点测试while ago测试RTTI的多个dispatch和vtable之间的差异。在释放模式下,在两百万次迭代中完成的三个对象(两个vtable调用)之间的分派需要62毫秒。这是不值得担心的方式。

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只需要有一种注册和按类型名称筛选操作的方法。它搜索一个代表任何代表操作的函子,并使用它。函子然后any_casts到适当的时间,并执行类型特定的操作。

或使用访客框架。以上就是这样一种变化,但是在一个非常普通的层面上才能真正符合要求。

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

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

3

(编辑为简洁起见。)

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

  • CPU:指令/数据缓存大小,结构和行为,特别是任何智能预取
  • CPU:分支预测和推测执行行为
  • CPU:罚用于错误预测转移
  • 编译器和CPU:可用性和rel有条件执行的指令ative成本(含分支机构的情况下帮助只包括一些指令)
  • 编译器或链接:优化,可能会改变你的代码,并删除分支

总之,作为Blindy在评论中说: :测试它。 =)

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

即使您找不到一个好的答案,您也会从应用该工具中学到一些东西。我经常发现在反汇编时也很有启发性(参见本主题中的一些其他答案)。

有些稍微推测音符:

  • 虚表倾向于导致的负载(这个+ 0),偏移量,第二负载,然后对寄存器的内容进行分支。你可以在其他答案中看到这一点。我熟悉的大多数CPU在预测寄存器分支时都很悲惨。
  • 布尔可能接近您正在使用的其他数据,因此可能已被缓存。分支目标也可能是固定的,因此对于预测和/或推测执行更加友好。
  • 在一些处理器上(这些日子很少见),加载bool而不是int会花费更多。我处理的ARM处理器上,我们偶尔会将vtables放在处理器核心的“紧密耦合的内存”中。大幅减少间接加载时间 - 就好像vtable始终处于缓存或更好状态。

正如您所提到的,通常的规则适用:做什么符合要求,首先是灵活/可维护/可读,然后优化。

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

无论是“面向数据的设计”还是“基于组件的实体”范例都有助于保持游戏,多媒体引擎以及其他大于平均水平的其他事物提高性能,仍然希望保持代码的有序性。 YMMV,当然。 =)

+0

从我提到数据导向设计+1。 – arke 2010-05-14 00:04:34