本文翻译自:http://stuff.lanowen.com/Ogre/2.0/OGRE.2.0.Proposal.Slides.pdf
由于本人才疏学浅,翻译难免有误,望各位不吝惜指正。
感谢作者为我们带来的分享:Ogre的不足与改进(Ogre2.0设计方案)
不足
- 大量代码的写法对缓存不友好,造成缓存命中率低:(
- 采用的场景遍历方式较为低效
- 顶点格式复杂,且不够灵活
- 固定功能管线 vs 可编程管线
- "setFog",等等
缓存失效
- 缓存友好真的非常重要
- Linux系统的路由包在有线情况下传输速率可以达到30Mbps,无线情况下可以达到20Mbps,而我们的WinCE系统在有线情况下传输速率勉强达到12Mbps,无线情况下6Mbps
- 经过研究,我们发现WinCE系统传输速率不如Linux系统的原因就是相关代码的缓存命中率低
- 我们改进了WinCE系统的相关代码,让其对缓存友好,最终让WinCE的有线传输速率达到了35MBps,无线传输速率达到了25Bps,比Linux系统快了20%
辣眼睛
大量代码对缓存不友好
对缓存的优化可以参考"Typical C++ Bullshit"
和"Culling the Battlefield"
寒霜2引擎使用结构体数组,SIMD和conditional move(条件移动指令)来对缓存进行优化
在Ogre中有大量代码容易导致缓存失效
在2000年时,因为ALU数量有限,缓存失效还不是性能的最大瓶颈
但随后几年,随着ALU数量的大幅增加,内存和带宽成为瓶颈,缓存友好变得非常重要
可以参考"Pitfalls of Object Oriented Programming"
对于x86和x64处理器,它们的分支预测功能可以在一定程度上缓解缓存失效引发的性能问题
但对于android和iPhone,以及主机所使用的处理器,条件分支导致的缓存失效对性能影响较大
好的代码应该避免条件分支
如果可以,分别实现条件分支所对应的不同情况
良好设计的游戏引擎,它的子节点的位置信息应该由父节点偏移计算得到,并且这一偏移计算在每帧应该只进行一次
如果在一帧中需要多次更新子节点的位置信息,应该手动调用_updateFromParent或直接调用_getDerivedPositionUpdated。出现这种情况,需要我们对引擎的内部实现有十分清晰的认识
缓存失效和指令流水线阻塞
指令流水线阻塞
浮点运算和条件分支不能同时进行
应该尽量使用conditional move(条件移动指令)来消除不必要的条件分支,从而避免指令流水线阻塞
对于PPC架构,可以使用fsel指令
对于x87 FPU,可以使用fcmov指令
虽然,直到SSE5才引入conditional move(条件移动指令),但我们可以在SSE2下使用cmp和mask指令来模拟它。
参考"Down With fcmp"
使用conditional move(条件移动指令)避免指令流水线阻塞
"fsel"函数编译后可能会根据硬件架构调用不同的fsel/fcmov/sse指令。最好查看编译器生成的汇编代码,确保fsel函数内联了正确的汇编指令。
使用SSE2来实现conditional move(条件移动指令)
实现方法:
- mask=cmp(condition1,condition2)根据比较结果建立mask
- t1=arg1&mask
- t2=arg2&~mask
- r=t1|t2
总共需要4条指令
使用SSE2来实现conditional move(条件移动指令)
实现方法:
- mask=cmp(condition1,condition2)根据比较结果建立mask
- t1=arg1&mask
- t2=arg2&~mask
- r=t1|t2
总共需要4条指令
一些SSE架构实现会在内部标记xmm寄存器当前存储的是浮点数据还是整型数据,因此,在使用浮点xmm寄存器进行整型运算(比如|或运算)可能会造成一定的性能问题(整型运算前标记xmm寄存器存储的是整型数据,之后将其标记为存储的是浮点数据)。
这也是为什么MOVAPS(float)指令看起来和MOVDQA(int)指令功能一样,却并非多余,合理使用它们可以避免不必要地更改寄存器的内容标记信息。
使用SSE2来实现conditional move(条件移动指令)
更好的实现:
- mask=cmp(condition1,condition2)根据比较结果建立mask
- t=arg2-arg1
- t=t&mask
- r=arg1+t
同样是4条指令
使用SSE2来实现conditional move(条件移动指令)
更好的实现:
- mask=cmp(condition1,condition2)根据比较结果建立mask
- t=arg2-arg1
- t=t&mask
- r=arg1+t
加法运算和减法运算都是代价极小的运算。新的方法同样使用了4条指令,但只有一个寄存器会被从浮点标记为整型,又从整型标记回浮点。而之前的实现,需要更改所有寄存器的标记信息。
但是,新的实现需要保证arg1的值不是nan或inf!(可以使用断言来保证这点,同时维护旧实现备用)
过早优化是万恶之源
过早优化是万恶之源
不认同!
每当见到加载窗口,我都会觉得或许我们应该更早地进行优化
过早优化是万恶之源
不认同!
每当见到加载窗口,我都会觉得或许我们应该更早地进行优化:)
- 进行这些优化并非过早,优化后得到的提升也并不微小
我们现在要优化的ogre代码已经10+年历史了
SIMD,并行化,缓存友好的设计现在已经是行业标配(CryEngine,Frostbyte等引擎已经广泛应用这些技术)
从这些优化手段获得的性能提升是非常巨大的
不相信?
可以看到Orge占用了大量CPU时间!
顺便一提,Distant Souls.exe有40次采样命中逻辑线程等待固定帧率(16ms每帧)的wait函数。它的渲染线程则以帧率可变的方式执行。
可以看出变换操作,动画,以及视锥体剔除占用了大量的CPU时间
还不信?
Distant Souls(Ogre)
- Intel Core 2 Quad Extreme X9650 3.0Ghz
- AMD Radeon HD 7770 1GB RAM
- 4 GB RAM
- 1280x720,未开启MSAA,最高质量渲染
Distant Souls使用了2个线程。其中一个线程只运行Ogre,另一个执行逻辑和物理运算
Distant Souls(Ogre)
- Intel Core 2 Quad Extreme X9650 3.0Ghz
- AMD Radeon HD 7770 1GB RAM
- 4 GB RAM
- 1280x720,未开启MSAA,最高质量渲染
Distant Souls使用了2个线程。其中一个线程只运行Ogre,另一个执行逻辑和物理运算
Assassin's Creed 2
- Intel Core 2 Quad Extreme X9650 3.0Ghz
- AMD Radeon HD 7770 1GB RAM
- 4 GB RAM
- 1280x720,未开启MSAA,最高质量渲染
- Intel Core 2 Quad Extreme X9650 3.0Ghz
- AMD Radeon HD 7770 1GB RAM
- 4 GB RAM
- 1280x720,未开启MSAA,最高质量渲染
可以看出Assassin's Creed 2比Distant Souls多了3倍的渲染对象,帧率却更高,显然,我们的引擎设计问题很大。
Distant Souls的渲染线程渲染一帧花费了43ms,该线程的空闲时间(包含在43ms内)基本都被GPU占用
逻辑和物理线程按照每帧16ms执行(解锁固定帧率后可以节约8ms)
渲染线程根据来自其它线程的命令更新可见对象,并调用renderOneFrame进行渲染
帧率较低的主要原因可能是缓存失效和复杂的后处理(接下来,我们会讨论为什么它不高效)