在最近的项目中,由于时间紧任务重,我果断放弃了引擎原来有点畸形的多线程设计,退回到单线程。最后事实证明,这是一个明智的决定,不但效率没有降低,同时那些永无止境的多线程bug也一并被消除,系统稳定了很多,最终项目得以在deadline之前有一个安全的交付。
1.是否多线程
去掉多线程的主要原因是,原来的多线程设计本身就是一个未经过深思熟虑的设计,它几乎是在项目开发的中后期突然插入的。整个引擎的最初设计没有考虑多线程,现在突然引入了多线程,而又没有经过精心的设计,那结果就是各种多线程bug扑面而来,让开发人员焦头烂额。事实是,如果需要多线程,那最好应该从项目设计架构的第一天开始就仔细考虑。
直观上感觉,好像使用多线程就会带来性能的提升。其实不然,如果没有经过精心的设计,随便引入多线程,只会适得其反。不但性能没有得到提升,而且引入多线程后带来的庞大开发代价和潜在的多线程bug,会让你和团队崩溃到吐血,典型的吃力不讨好的事情。相比于单线程,引入多线程的设计需要带来2到3倍的开发和测试代价,在这么一个时间紧急的项目上,在项目的中后期突然引入多线程真是个大错特错的决策,应该果断拿掉。在这篇博客文章“一种3D引擎的多线程设计方案”里,作者就提到,“多线程的引入使引擎变得更加复杂,不良的设计带来的性能提升非常有限,甚至在单核环境下还会出现明显的性能下降”。
从这篇Unreal引擎工程师Tim Sweeney的访谈文章中,我找到了在我的情况中应该拒绝多线程的另一个重要原因。
Tim提到:
For multithreading optimizations, we’re focusing on physics, animation updates, the renderer’s scene traversal loop, sound updates, and content streaming. We are not attempting to multithread systems that are highly sequential and object-oriented, such as the gameplay.
This is also why it’s especially important to focus multithreading efforts on the self-contained and performance-critical subsystems in an engine that offer the most potential performance gain. You definitely don’t want to execute your 150,000 lines of object-oriented gameplay logic across multiple threads – the combinatorical complexity of all of the interactions is beyond what a team can economically manage. But if you’re looking at handing off physics calculations or animation updates to threads, that becomes a more tractable problem.
Tim的主要思想是,游戏引擎多线程应该关注在“独立自主且性能关键的子系统”上,比如物理系统或者是动画系统,这会带来最大的性能提升。而在我的项目中,多线程是放在了脚本(scripting)层,这正中了上面Tim所提到的多线程的雷区——gameplay。脚本是处理gameplay层次的逻辑的,它是高度顺序且面向对象的,并且与其他各个子系统有着千丝万缕的关联。正如Tim所说的,将脚本逻辑放到多线程来执行所带来的交互的复杂度已经超过一个团队的承受范围了。
从这篇文章中我们也可以获知,像Unreal这样知名的商业引擎,也是到最新的第3版(这种称为下一代的游戏引擎)中才开始支持了多线程,这从一个侧面反映了多线程技术在游戏引擎中是一项高级而复杂的技术。主要原因这受制于硬件的发展,多核CPU的PC是到近些年才开始在市场中变为主流,在单核CPU中使用多线程会带来额外的开销,所以在传统的游戏开发中,往往都是单线程的。(这里是一个很好的关于用户软硬件使用情况统计的网站,不过遗憾的是统计数据基本都是来源于国外,不带中国人玩的,对于国内没什么参考价值)
另外对于游戏引擎引入多线程的考虑,还有很重要的一点是,我们需要认真的考虑当前引擎的性能具体是受限在哪里,是CPU还是GPU,我们该在哪里进行优化。因为GPU渲染在游戏中是如此的重要,所以在大多数情况下,很可能GPU才是主要的性能瓶颈。只有真正的当物理,AI等这些模块变得越来越重要,并且约束系统性能时,我们才需要仔细考虑是否需要为游戏引擎带来多线程的可能。
总之,多线程就是一把双刃剑,在决定使用它之前,一定要经过明智而审慎的思考和设计。
2.如何多线程
对于游戏引擎的多线程设计,最近读到了一些相当不错的文章,在这里作个整理。
Gamasutra上的Multithreaded Game Engine Architectures是一篇非常基础且有意思的文章。
该文章首先指出,程序并行有两种主流的方式,第一种称为函数并行(functional parallelism),另一种称为数据并行(data parallelism)。所谓functional parallelism,即将程序中不同的任务分配给不同的线程并行执行,而data parallelism则是将数据划分到不同的线程上并行执行,但每一个线程执行的是相同的任务。这样的分类比较利于我们更好的理解程序并行的行为,我们可以在很多地方看到有这样的分类,比如在Intel的这篇关于并行引擎架构的文章中,提到了类似的functional decomposition和data decomposition,而John Owens在Siggraph 2008的talk “Parallel Programming Models Overview”中,也提到了这样的分类,他称之为task parallelism和data parallelism.
基于这些并行方式,该文章提出了游戏引擎多线程的3种架构。
前两种架构都是基于functional parallelism的方式,第一种架构称为同步的函数并行模型(synchronous function parallel model),是最常见的一种游戏引擎多线程方式。该方式保留传统的game loop,将完全独立的子系统的任务分离到辅线程,并与主线程并行执行,每帧作同步。Intel的Multi-threaded Rendering and Physics Simulation这篇文章就是这种方式的典型。
第二种架构称为异步的函数并行模型 (asynchronous function parallel model),是一种非常新颖而前瞻的架构,它脱离了传统的game loop,各个子系统跑在各自的线程上,并按自己的节奏拥有独立的game loop,该架构拥有非常好的可扩展性,但难点就是不同子系统之间的数据同步问题。对于这种架构方式,Intel的这篇Designing the Framework of a Parallel Game Engine是一篇非常棒的文章,极力推荐,该文章提出了Free Step Mode和Lock Step Mode两种并行执行的模式,不仅仅是并行架构,从这篇文章中还能学到其他很多关于游戏架构的知识。
第三种架构是基于data parallelism的方式,文章中称之为数据并行模型(data parallel model)。这种方式我们并不陌生,比如我们经常听到的GPGPU就是这种方式在GPU上的体现,另外我们所熟知的SIMD(Single Instruction Multiple Data),SPMD(Single Program Multiple Data)技术都是这种方式的典型。该方式可以获得最大的并行性,但缺点是采用数据并行的系统往往非常难于设计,通常需要修改原来的面向对象的设计。可以看看Intel的这篇TickerTape的文章,利用SIMD技术提升性能,但却需要破坏原有的类的设计,将结构数组(Array of Structures)改为数组结构(Structure of Arrays)。
关于更多的游戏引擎和多线程的资源,可以参见这里。
Benny, 什么时候有空写写关于场景编辑器的东西。
有空会把这段工作整理整理的,when it is over.