移动游戏GPU性能优化方法论
引言游戏作为复杂系统,性能会被各个模块综合影响。在某单一的点优化,不一定会带来整体的性能提升。所以做优化的第一件事情就是分析。只有通过对系统的综合分析,找到瓶颈点才能使优化有的放矢。既然是系统分析,自顶向下总是一个好的方法。随着不断向下深入的分析,我们最终会找到影响性能的根因。
现在,我们以GPU是性能瓶颈作为出发点,我将分享我常用的分析思路,希望可以为不熟悉这一领域的同学们,提供一个观察的角度,以便大家能够找到问题的根因。也欢迎大家指出文章中的不足,共同学习。
阅读这篇文章的时候,最好有一点GPU相关的知识。后续我也会更新一篇讲解移动GPU体系结构的文章,欢迎催更~
目标
对于优化来说,目标非常重要,它将决定你最终找到的根因。常见的三种优化目标是质量、性能、功耗。我知道你们要说话了:
事实上往往我们只能达成其中的两个甚至一个。怎么说呢,就一破手机,要啥自行车啊(
在硬件能力有限的情况下,优化可以说是一门取舍的艺术。由于移动设备散热能力、电池大小以及需要手拿着玩等等的限制,功耗和性能的优先级往往会高于质量。毕竟拿着暖手宝看华丽的PPT也算不上是玩游戏了(
分析和优化
基于以上目标,我们做自顶向下的分析,逐渐深入细节。
观察数据,确定优化范围
虽然开头为文章划定的范围是GPU,但是还是简单过一下其他的。大部分情况下,我们关注三种器件的使用情况:CPU、GPU、DDR。我们需要一段时间内系统的一些数据,来帮助我们判断优化范围。这些数据可以通过一些工具获取,比如systrance、streamline、snapdragon等等。
[*]CPU、GPU、DDR运行时的频率。
当硬件工作频率不稳定的时候,可能会导致帧率抖动,有些帧被拉长,有些帧会被延后、缩短甚至跳过,造成视觉上卡顿的感觉。如果硬件工作频率一直很低,性能得不到释放,帧率可能会一直很低。而持续高频率工作,容易造成温度快速上升,触发硬件设备温控限频,导致瞬间掉帧。如果此时空转率高,也会造成能源浪费。DDR频率低,则会导致数据访问的latency过长,CPU和GPU等待返回结果空转浪费能源。
[*]DDR带宽
访问DDR是非常耗时耗电的操作,大量的DDR带宽表示游戏大概率有性能功耗风险。
对于帧率不稳定的情况,可能是硬件的调频策略有问题,或者游戏每帧的负载差距太大。如果设备经过root了,可以通过锁频来判断具体是哪种情况。如果确定是游戏本身有问题,我们则需要进行进一步分析。
分析游戏行为
通过工具,我们可以抓取一系列系统运行时候的状态和线程执行时间。原则上,我们从执行时间最长的部分入手。不过开头我们约定了范围是GPU,我们现在就认为GPU是执行时间最长的部件,开始进入主题。
我们首先会关注一下空转率,因为空转代表问题可能还不在GPU身上。也许是每次Draw Call的任务量太少、或者DDR延迟太高。当GPU真正的在满负载运行了还是很卡,我们就需要分析游戏在GPU上的行为了。同样,我们需要一些工具,我经常使用的是RenderDoc和Mali离线编译器。ios上还有神器xcode。
[*]PASS分析
一个pass可以理解为一次渲染目标的切换,或者一次强制刷新。游戏中的一帧往往会有多个pass,每个pass里又包含多个Draw Call或者graph api调用。我们通过RenderDoc抓取游戏一帧的绘制流程,提取游戏行为和相关资源使用情况,可以整理成类似下面的表格方便分析:
RenderPass功能描述输入输出Draw Call 0z pre passtranglestexture0zbuffer............Draw Call n~mgeometry passTexture nabedo
Texture m metalTexture kroughnessTexture aGbuffer
Texture bTexture c上述表格按功能对pass进行分组,列出我们关注的资源,之后看到类似资源更新未使用,或者公用资源却分pass提交的情况,就可以一眼看出了。同时,对功能的描述也让我们清晰的掌握了整个渲染流程,为后续分析提供思路。
[*]Draw Call分析
在了解整个游戏的行为之后,我们针对每个pass,分析各个Draw Call和graph api的行为,并试图优化其中一部分。我们优先消除冗余,然后遵循“加速最大概率事件”的原则,确定优化的优先级。
从全局来看:首先识别Draw Call数量是不是太多,考虑合并。然后考虑Overdraw是否严重,通过剔除、遮挡查询等、draw call排序(提高early z成功率)等方式可以进行优化。之后是三角形数量,太多的三角形,可能造成vertex shader、tiler、culling方面的压力。其他方面,我们需要识别资源是否有冗余,流程是否可以合并,资源是否可以压缩、降低精度、降低更新频率等等,这可以缓解带宽压力。
从局部来看:我们需要识别Draw Call中使用到的算法,并优先消除冗余的计算、寄存器使用、数据读写等行为。然后可以考虑是否有硬件和API特性可以加速计算或者读写过程(比如subpass)。在这些无损的优化完成后,我们可以考虑使用不同的算法进行上下位替代,以优化在不同的硬件平台上的表现。更进一步,我们可以对Shadre代码本身的质量进行分析,识别其中的寄存器溢出、循环、分支、不必要的高精度、有副作用的操作等。
[*]Shader分析
通过Mali 离线编译器,我们可以对Shader有一个较为清晰的认识。
如图是某个Shader 进行离线编译后的一些指令数据。包括寄存器使用数量、寄存器溢出大大小、半精度算术的比例以及各种指令的Cycle。其中FMA、CVT、SFU都是算术运算指令,LS是读写存储的指令,V表示插值运算,T代表纹理采样。最后的Bound也可以清楚的指出Shader的瓶颈是哪些指令。
对于各种指令的优化方法,视情况而定吧。不过还是有一些通用的思路的。首先这些指令在一个线程内是串行执行的,优化任何一种都能带来整体性能的提升。不过LS指令比较特殊,可以认为是并行执行的。习惯上,我们从cycle最多的指令开始优化。
寄存器溢出:
寄存器使用的数量,会影响一个execute engine中的同时工作的线程数量(这个在不同架构上情况可能不同,对于execute engine中共享物理寄存器的架构来说,都是成立的,因为你把寄存器都用了,别的线程就分不到了嘛)。而寄存器溢出,还可能导致LS指令上升。可以通过降低变量精度、缩短变量生命周期、简化shader算法等方式来优化。在valhall架构(麒麟9000同款)中,使用32给寄存器可以保持最大的线程数,超出后,线程数减半。
LS指令:
LS是访问memory的指令,可能来自buffer、texture、数组、结构体的读写以及寄存器溢出(uniform buffer和编译器可以确定下标的小型数组访问,会被尽可能的优化为寄存器访问)。LS指令可能会产生DDR带宽,需要尽量减少,或者遵循局部访问原则,尽量提高Cache命中率,降低访问时延。
FMA、CVT、SFU:
计算指令一般就得按照实际情况来简化公式、合并计算了。
T指令:
T指令来自纹理采样,同样的,尽量将采样范围集中,可以提高命中率。某些情况下也可以用插值代替采样。值得注意的是纹理的采样是Texture Unit进行的,Texture Unit和LS Unit独立享有L1 Cache。Texture Unit的L1 Cache缓存的并不是地址相邻的像素,而是空间上相邻的像素。所以应该尽量集中采样范围,跨行的纹理采样也会从L1 Cache受益。
Uniform Computation:
还有一些变量在多个线程中的计算结果都一样,这种变量应该由CPU计算通过Uniform变量传到GPU。
用副作用的操作:
副作用会造成GPU某些优化失效,比如early z、面剔除等。这些行为有:写入SSBO、写入Image、原子操作、读取color buffer、修改z buffer,应当慎重使用。可以考虑拆分有副作用的操作和其他shader代码,以保证其他代码正常被优化。
总结
游戏的行为总是多种多样的,优化的姿势千奇百怪,分析的方法万变不离其宗。不管怎样,最后都是硬件去执行,所以我们分析负载、指令总是没错的。本文并没有给出一些具体的算法,而是希望给出一些观察的视角,来帮助做优化的同学们来审视游戏,以便找到问题产生的根因,并寻找适合的方法来解决它。毕竟算法可太多了,光一个遮挡剔除就有预计算的,CPU的,GPU的好几种,压缩算法更是不计其数。到底哪种好,还是得看游戏的实际情况,综合考虑各个器件的负载。
原创内容,转载前还请先找我申请授权~
页:
[1]