[译]07.打造灵活高效的粒子系统——优化入门

07_1

是时候开始改进我们的粒子系统了。目前我们已经可以让粒子动起来并用OpenGL做一些基础的渲染。你可以在这里看一些截图和视频 。但是这个系统最多可以跑多少粒子?效率高不高?有没有什么问题?热点和瓶颈在哪里?这是我们首先需要整明白的。

先了解一些优化程序背后的理论知识,然后再想办法应用到我们当前的粒子系统的示例上。

转载请注明[本文链接][原文链接],谢谢!

  • 引言

作为游戏开发者,你希望越来越多的……各种东西。更多的像素,更多的三边形,更高的FPS,更多的物体,机器人,怪物等等。很遗憾的现实的资源不是无穷无尽的,必须做出妥协。优化就是帮助我们减少性能瓶颈,获得一些隐藏在代码之后的资源。

优化不能基于随机的猜测,“oh,我把这段代码用SIMD重写的话,游戏运行的就会快一些”。你怎么知道这段代码导致了性能问题?把时间和精力投入到这段代码上值得吗?会不会导致相反的效果?最好有个清晰的方向做指引。

为了更好的弄清楚需要优化什么,我们需要测出当前游戏的“基线”。就是说需要测量系统的当前状态,找出热点瓶颈。考虑下你想改进的地方……然后……开始优化代码。这个过程也许不够完美,但是至少最小化了潜在出错的可能,同时最大化收益。

当然这个过程不可能一次就完成,每次修改了一点,就再从头开始。一次只改一点,循序渐进。

最后你的游戏应该能正常运行(希望没有引入任何新BUG),而且它比以前运行的快N倍,如果你优化的正确,这个N应该能测量的。

  • 软件优化过程

根据这本这本书的介绍,优化过程应该是这样的:

  1. 建立基准
  2. 找到热点和瓶颈
  3. 改进
  4. 测试
  5. 回到起点

07_2

这个优化流程不应该是全部实现做完之后才做(通常那时候也没时间了),应该在项目开发过程中就开始了。我们这套粒子系统在开始之前我就考虑可能的优化方案了。[译者注1]

  • 基准

有一个好的基准是非常重要的,如果做法不对可能导致这个优化过程变成了浪费时间。

摘自《The Software Optimization Cookbook》:

基准是指一个程序或过程应该:

  • 客观的评估应用程序的性能;
  • 应用程序的行为是可重复的,以利于使用性能分析工具分析;

几点根本的要求如下

  • 可重复的 – 每次运行都可以得到相同的结果。
  • 代表性 – 大量使用针对整个应用程序的用例,如果只关注其中一小部分运行结果,那么这些用例毫无意义。对于游戏这种应用来说,应该采用最常见的场景,或者物品最多的场景来作为基准。物品多的场景可以代表物品少的场景。
  • 容易运行 – 你绝对不愿意花数小时的时间跑一次基准测试。运行一次基准测试当然比运行单元测试麻烦,但是也不能太慢,一定要尽可能的快。另外所产生的报告一定是简明扼要的,比如FPS报告,耗时报告等等,不要让这些信息淹没在几百条记录里。
  • 可验证 – 确定基准产生的是有效的有价值的结果。
    • 找出热点和瓶颈

07_3

当你运行一次基准可以得到运行报告,也可以通过性能分析工具获得程序运行的详细性能分析。

只有数据还不行,更重要的是要理解、分析并得出结论。你需要找出是什么原因导致程序没有全速运行。

总结一下:

  • 瓶颈 – 程序内部的,一些导致整个程序运行速度降低的模块或子程序。就相当与整个链条中最弱的一环。比如你有很强力的GPU,但是内存带宽很低,就无法快速给GPU传输数据,它必须等待,因此效率会降低。
  • 热点 – 系统中做关键的密集计算的模块或子程序。如果你优化了这些模块,那么整个系统将运行的更快。比如CPU计算量很大而GPU较轻松,就可以把一部分CPU的运算放到GPU里。

这部分是最难的,简单的系统很轻松就能看出问题,但是大规模软件就能难了。导致运行速度慢的原因有可能是一个小函数,有可能是整个设计问题,也有可能是某些算法用错了。

通常采用至上而下的方法分析。比如:

帧数很低,测量CPU和GPU的利用率,如果是CPU过高,考虑下各子系统,到底是逻辑、AI还是物理模块导致的?还是因为驱动程序无法处理这么多DrawCall?如果是GPU过高,考虑是vertex shader还是fragment shader导致的?不停的细分下去。

  • 改进

07_4

现在到有趣的部分了!改进一些东西让程序跑的更快:)

以下是可以改进的内容:

  • 系统层 – 看下整个程序的利用率,有没有资源处于闲置状态?有没有CPU或GPU等待的情况?能不能利用多核优势?
  • 算法层 – 是否使用了合适的结果和算法?也许能把O(n)的算法优化到O(lgn)。[译者注2]
  • 微观层 – 这是“最有趣”的部分,但是要确定前面两个都已经没什么可优化的了。如果你确定设计上不能再提高了,就使用一些脏代码技巧(dirty code tricks)来提高运行效率。[译者注3]

注意:在把所有代码写成汇编以前先用工具分析下。现在的编译器已经非常强大了,可以把代码优化的很好。另外还一个可能的问题,有一些优化技巧也许不能用到其他平台。

  • 测试

当你完成一些修改之后,测试下系统的效率,速度是提高了50%还是变得更低了?

除了测试效率之外,更重要的是确定没有改坏任何东西!将系统提高10%的运行速度固然很好,但同时老板肯定不希望你引入很难发现的BUG。

  • 回到起点

07_5

当你完成了整个流程,游戏也比以前运行的更顺畅,应该跑一遍基准,再从头再来。每次只改进一点,循序渐进的改比一次改很多很复杂东西要好很多。小步快走不容易犯错而且即使犯错也容易撤销修改。

  • 剖析工具

主要方法:

  • 自定义时间统计 – 你可以创建一个独立的模块,这个模块可以进行计数和时间统计,然后在重要的子系统中调用,由这个模块生成调用堆栈和统计信息,供以后分析。
  • 仪器式 – 这些分析工具会在你的程序里插入特殊代码段,所以它们可以测量程序的运行过程。
  • 植入式 – 这种方法是工具采用API钩子(intercepts API calls )的方式(比如OpenGL的glIntercept),之后分析这些调用。
  • 采样 – 工具每隔一段时间中断一下程序,然后分析调用堆栈。这种方法比instrumentation要轻量一些。

下面是一些专业的分析工具:

1自动化

我也许不需要写这部分,自动化程度越高就越容易做优化工作。

自动化几乎可以应用到任何地方,测试,设置程序参数,启动程序等等。

2 有乐趣!

上面的过程听起来很专业很无聊,优化过程还有一个很重要的影响因素:很有趣。

你想通过犯点错误,猜测下需要优化的地方,学习点新东西。即使优化错了,你还是能获得一些经验的。

日常工作中也许你没有时间做这个,但是尝试下业余爱好的项目呢?

你的优化经验越丰富,你的程序跑的就越快。

  • 粒子系统的基准

07_6

目前为止我们只谈到了理论和一些基础知识,现在我们把这些应用到我们的粒子系统上。

粒子系统只是游戏引擎的一个子系统,我们的粒子系统完全是用CPU算的,所以不需要统计CPU和GPU的利用率然后分析瓶颈再哪边,直接开始CPU这边的优化就好了。

另外,目前我们的系统非常简单,我们可以直接进行微观层面的优化。换句话说我们可以只考虑代码级修改(code hacks)。当然作为作者我假设目前这个设计是很合理的,不需要修改:)

我写了两个小程序做测试,一个是显示三个粒子效果的(“3effects”) ,一个是测CPU的CpuTest。

这个小程序只是简单的创建3个粒子效果(这里有视频演示)。

  • 隧道效果(Tunnel
    • 使用位置、颜色、速度和时间generator。
    • 使用颜色和欧拉变换的updater 。
  • 引力效果(Attractors
    • 3个发射器(emitter),分别使用曲面速度、颜色和位置的generator。
    • “速度—颜色”的更新器(updater,颜色根据位置变化),引力更新器(attractor updater),欧拉更新器(euler updater),和时间更新器(time updater)。
  • 喷泉效果(Fountain simulation
    • 使用量位置、颜色、速度和时间的generator。
    • 使用的updater有:时间、颜色、欧拉和地面(粒子碰到这个水平面会反弹,模拟简单的物理)。

我觉得这一组特效可以发现很多系统的问题,只关注一个问题有可能会被误导。

这两个测试程序的粒子创建和更新的代码是一样的,但是渲染的不同。3effects使用OpenGL渲染,很漂亮的视觉交互,这个小程序将来也可能用于测量GPU运行效率。

cpuTest只针对测试CPU,只是换了个假的渲染器,这样啥都不用改,就可以用复用所有的特效代码。

CpuTest的基准

以下是节选的关键代码(简化了一下):

for(size_t step =0; step < PARTICLES_NUM_STEPS;++step)

{

size_t numParticles{ START_NUM_PARTICLES + step*NUM_PARTICLES_STEP };

for(constauto&name : EFFECTS_NAME)

{

auto e =EffectFactory::create(name);

 e->initialize(numParticles);

// start timer

for(size_t frame =0; frame < FRAME_COUNT;++frame)

{

            e->cpuUpdate(DELTA_TIME);

}

// end timer

}

}

上面的代码做了这些事:

  • 模拟START_NUM_PARTICLES到 START_NUM_PARTICLES + START_NUM_PARTICLES * NUM_PARTICLES_STEP个粒子;
  • 使用EffectFactory创建3个不同的粒子效果;
  • 初始化特效;
  • 运行FRAME_COUNT次update()函数。(FRAME_COUNT默认是200);
  • 测量执行的时间,并且把这个时间输出到控制台(代码中这里是用注释表示的)。

运行这个测试可以得到下面的结果:

count    tunnel    attractors    fountain

1000     247       349           182

11000    2840      6249          2112

举例说明下,11000个粒子,引力效果做200次更新需要6249毫秒。

  • 结果

PC Spec: Core i5 2400, Intel Sandy Bridge, 4Gb, VS 2013 Express

测试机器配置:Intel Sandy Bridge Core i5 2400 CPU,4G内存,VS 2013 Express版本。

07_7

引力效果耗时比重很大, AttractorUpdater::update(double dt, ParticleData *p)这个函数是个热点。

同样EulerUpdater也消耗的大量时间。

另外统计3effects程序的运行帧数如下:

count    tunnel    attractors    fountain

200k     59 fps    38 fps        59 fps

300k     30 fps    30 fps        59 fps

400k     29 fps    30 fps        32 fps

500k     19 fps    20 fps        29 fps

FPS受draw call和GPU缓存更新的影响,如上面统计的喷泉效果貌似当粒子数在30~40万之间有瓶颈。

从这个结果看起来,100万个粒子跑到60帧(至少45帧)不容易啊。

  • 系统中需要改进的地方

1微观层面

第一次猜测:

  • ParticleData::wake()函数中if (m_countAlive < m_count)可以移除,另外因为刚扩展了存活粒子的数量,也不必调用wake函数。这点可以参考讲容器和generator的那两篇文章。
  • ParticleData::kill()函数中if和上面的情况一样,kill()函数是把一个粒子标记为死亡,并且把它和最后一个活着的粒子交换位置。
  • ParticleData::swapData()交换粒子数据,死掉的粒子就不在需要更新了。

原始版本:

count    tunnel    attractors    fountain

291000   92712     183797        67415

更改之后:

count    tunnel    attractors    fountain

291000   89868     182029        66776

-3%       -1%           -1%

从结果来看还算可以吧,只是一些小的逻辑调整。(也许你能找出其他需要修改的地方?)

这种“乱打一通”的修改对帧数没有很大提高,所以我们需要再深入一些。

2 编译器优化

是不是有一些编译器(我目前用的是VS2013)参数,改下设置后可以提高一点帧数?我将在下一篇文章里详细讲这个。

3 指令和内存访问方式

使用SIMD指令可以提高计算效率,在上面例子中大多只是缩放操作用了这种指令。同样,我将会单独写一篇文章介绍这部分。

4 其他

也许我们可以考虑将粒子系统改成多线程的,利用并发优势,不过目前我想只做单线程的优化。

还有就是关于渲染的了,我们需要研究GPU缓存的更新、数据传输和DrawCall等,让它们更有效率。

  • 下一步

目前为止我们迈出了虽然小但是重要的一步。我们有理论的支持,有对比的基准同时也找出了一些潜在的问题,通过这些信息我们可以继续进行优化。下一次我将写一些关于编译优化的内容,也不不用改一行代码就能提高运行效率。

  • 该你了

  • 你是怎样优化你的游戏的?
  • 用了什么工具?

请在这篇帖子这篇帖子里回复,谢谢!

  • 引用

1:有个观点说提前优化是万恶之源,那是指在写代码的阶段去做优化,作者这里是指在设计阶段就考虑优化,是不矛盾的。

2:有个观点说算法在一开始写的时候就已经决定它的优劣了,以后无论修改多少次,都只是改善而不是本质提高。 除非推翻重写新的算法,但这已经不是以前的算法了。

3:脏代码技巧,程序员都是些追求条理性和严谨性的家伙,他们总是尽自己最大的可能来保证自己的代码整洁而漂亮。但是当情况紧急、日程表不容修改且游戏需要交付的时候,“把他做完”就凌驾于所有对优雅的追求之上。

原文地址:http://www.bfilipek.com/2014/07/flexible-particle-system-how-to-start.html

No Responses

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

You must enable javascript to see captcha here!