6

游戏性能优化杂谈(十六)

 2 years ago
source link: https://zhuanlan.zhihu.com/p/390797736
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

游戏性能优化杂谈(十六)

C++话题下的优秀答主

最近接触到一个用比较新版的unity做的项目(2019),由于SRP的导入,CPU端的不同核心之间的负载均衡情况比起老版本(2017)有很明显的改善。不再是一核有难多核围观的情况了。

这主要是因为unity的SRP将很多渲染相关的准备工作,比如command buffer的生成等都用job system进行了分散。也就是新的unity不再是老的那种由主线程生成unity中间绘制命令,然后由设备线程翻译成本地渲染命令并提交那种结构,而是在job system当中完成相关的准备之后,直接在各个job当中生成对应的本地渲染命令片段,然后这些片段由另外一个专门的提交线程分多次提交给GPU。

这种结构极大地减轻了主线程的压力,并提高了CPU多核心的利用率。

然而,这一改动似乎也引入了新的问题。

首先,据我观察unity的object并非线程安全,这一点应该是并没有发生改变的。所以,为了能够让多线程处理渲染命令队列,似乎是存在着很多数据的拷贝(memcpy),并且在实际发动job worker之前,似乎存在着一些对于object进行解耦合的操作。不过这些都只是从运行时行为进行的猜测,我并没有分析源码。(我也没有源码阅读权限)

而且,由于各个job worker是独立生成绘图命令片段,引擎可能比较难以追踪job worker之间的渲染状态变化,unity采用了最为保守的做法:在每个渲染命令片段头部插入对于GPU状态的重置,并且在每个命令片段的尾部插入对于GPU cacheline的回刷。简单来说,这就是在每个片段前后加入了等待同步命令,而且从根本上杜绝了不同片段工作量在GPU当中并行执行的可能性。比如,我们发现了在一个shadow pass当中,存在着大量这样的重置和回刷,虽然其实它们都属于同一个subpass,这些都是不需要的。

此外,unity引擎自身的效率提高,导致游戏本身的C#逻辑部分就显得更加突兀。由于游戏的C#逻辑需要频繁访问unity object,所以一般均为主线程内执行。这部分的逻辑往往难以并列化,所以在游戏逻辑执行的区间,依然是一核有难众核围观的情况。之前由于从头到尾基本都是这样也就还好,现在引擎自身的处理高度并行化之后,游戏逻辑部分就显得很突兀了。

而C#层用户逻辑的最为主要的问题依然是object的创建、初始化、GC、以及由于封装导致的对属性访问的层级开销。比如游戏当中需要频繁访问对象的位置信息,但是在Unity的C#里面你需要从上往下一层一层剥开,最终才能拿到这个叶子节点的属性,而不是像C++的结构体或者类那样,对象的基地址加上一个内存偏移就可以了。

特别是当有多个子类的时候,貌似运行时对于实例的子类类型的确定也是较为耗时的。

还有就是对于C#转IL2CPP的情况,IL2CPP里面有一些为了支援C#调试的功能,比如记录转换之前C#函数名,记录callstack什么的代码,也是相当耗费性能的。这些代码单次执行的时间可能也就是几个微秒,但是问题是它是在整个C#执行期间不断被执行的,是代码的热区,因此是值得好好优化的。

Unity官方版本对于PS5的高速IO支持也是非常初级的,基本上就是在PS4的版本上硬改,目前观测下来可能只发挥了原本应该可以达到的性能的1/10甚至更少。

对于大地形的支持方面,这部分我也不是很清楚是unity自带的还是开发商自己写的,总之地形的四叉树剔除是执行在C#层面的。像这种基础设施一般来说还是建议C++化,或者GPU化,siggraph上面有现成的资料可以参考。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK