

ARM Mobile Studio性能优化(二)
1LGheRnUvP
程序员
阅读 3430
2020年10月4日
透过Streamline来学习GPU的原理
ARM Mobile Studio性能优化(一) https://developer.unity.cn/projects/arm-mobile-studioxing-neng-you-hua-yi url
这篇文章我们开始继续 Streamline 的学习,透过 Streamline 的参数来学习 GPU 的原理是件,非常有意义的事情,在学习之前我们先来回顾一下 Mali 的 GPU 架构的历史。
第一代架构 Mali Utgard GPU,是 Mali 最早的 GPU 架构,它支持 OpenGL ES 2.0 核心包括 Mali-400 Mali-470 Mali-550 ,由于现在已经没有手机用这个架构了,所以可以不用看了。
第二代架构 Mali Midgard GPU,支持 OpenGL ES 3.0 和 OpenCL 支持的型号包括 Mali-T600, Mali-T700, and Mali-T800,现在可能还有一部分低端机在使用它。
第三代架构 Mali Bifrost GPU,支持的型号包括 Mali-G30, Mali-G50, and Mali-G70,我手上 的这台 Oppo R15 就搭载着 Helio P60 GPU 是 Mail-G72 Mp3 架构显卡由联发科生产。G70 系列显卡算是 2017 年的旗舰款了,现在可以算中低端性能机器。
第四代架构 Mali Valhall GPU,支持的型号包括 Mali-G5x and Mali-G7x,从 2018 年开始量 产。比如华为的 p40Pro 就搭载着麒麟 990,GPU 是 16 核 Mali-G76 GPU 由华为海思生产。
这里如果对硬件不太清楚的小伙伴可能会有疑问,ARM、高通、海思、联发科、Mali、华为、 小米、OPPO、台积电它们之间到底是啥关系?为什么所有手机 CPU 架构都使用 ARM 的?
ARM 厉害就在于它保持中立,自己不生产芯片,只卖 CPU 和 GPU 的架构。这样苹果、高 通、海思、联发科就可以购买它的架构后来自己生产。这就好比 ARM 是游戏引擎、游戏厂 商拿了游戏引擎的源码就可以开发属于自己的游戏引擎和游戏一样。
试想一下,如果 ARM 生产芯片,会不会给自家芯片加点什么特殊东西?那么谁还敢买它的 架构呢?所以保持中立是最正确的。当然苹果高通他们拿了 ARM 架构还会自己修改,这样 才能让自己的 CPU 进一步优化。
CPU 大家都使用 ARM 的架构,但是 GPU 上确不是。苹果和高通都自己设计 GPU 架构,高 通早年买了 AMD/ATI 的移动 GPU 方案自己定制了 Adreno,不得不说在安卓市场上最强的 芯片还是高通。2020 年 8 月份安卓手机性能排行榜前十名全是高通骁龙 865,即将登场的 高通骁龙 875 更是不得了。
华为的海思和联发科都是买了 ARM 的 CPU 和 GPU 架构,来自己生产设计芯片,华为厉害 就在于这样可以软硬件一起做,进一步提升手机性能。还有些其他厂商,比如小米 Oppo 等, 可以直接买了高通或者联发科的芯片来生产手机。这就好比我们做游戏,大厂都会买 Unity 的源码来定制一样,只有深度定制后才能发挥游戏的最高性能。所以说苹果才是深度定制的 最强玩家,从芯片的架构设计、软硬件、全都自己搞,这样的手机性能才能发挥极致。
有了芯片的架构还没完,总得生产出来吧,现在的芯片都是 7 纳米 5 纳米工艺,人眼都是 无法看见的,这时候就需要台积电了(不得不说台积电真是牛啊),可见从芯片的架构到消 费者中间有这么多环节。
在回到开发上,芯片生产商比如高通,显然不愿理透露太多芯片原理性的东西。因为它是直 接面对消费者,而 ARM 就不一样它是卖架构的,所以它会尽可能的透明原理,所以在 ARM 的官网上可以查到非常多的资料有助于我们学习,借助学习 Streamline 我也有了不少心得,希望更多的朋友一起讨论学习,可能有些东西我理解的也不正确,希望大家互相学习互相讨论。
OK 废话就不多说了,我们进入正题。首先我们来看看Mali现在主流的GPU架构Midgard 、 Bifrost、Valhall 的区别.
Bifrost 引入了 Index-Driven Geometry Pipeline (IDVS)翻译过来就是索引驱动几何管线。 先来看看早期的 Midgard 架构,Vertex Shading 走完后才进 Primitive Assembly 图元装配。 顶点着色里不仅需要处理坐标可能还需要处理别的,如果能单独把顶点拿出来提前处理,提前过滤掉需要被裁剪的图元,这样就能减少 Vertex Shading 的计算量。

Bifrost架构将Vertex Shading拆成两部分,在裁剪剔除(Clipping&Culling)之前先处理位置着色,这样就能提前过滤掉不再锥形体以内的图元,然后在执行通过的Varying Shading,这样就减少了原本顶点着色的计算量了。

Bifrost 架构的着色核心,注意看 Execution Engine

在看看 Valhall 架构的着色核心,注意看 Processing Unit。

可以看到原本 Execution Engine 的被改成了 Processing Unit,它将原本数学计算管线分成了三部分,FMA 处理复杂的数学运算、CVT 处理简单的数学运算、SFU 处理特殊功能。 Load/Store Unit:加载/储存单元,负责和纹理采样以外的内存访问、比如内存访问、缓存区访问、它还拥有一个 16KB 的 L1 缓存。
Varying Unit:差值单元,比如顶点 shader 里的数据,插值后才会返回给片元着色器上。 ZS/Blend Unit:混合单元,负责将颜色写入缓存中。
Texture Unit:纹理单元,负责所有的纹理内存访问,一个时钟周期会采样 2x2 也就是 4 个像素。 所以如果我们的游戏 bound 在了 GPU 上,那就一定是以上 5 个单元出了问题。通过 Streamline 我们就可以知道到底卡在了那里。Streamline的使用方法可以看我上一篇文章
https://developer.unity.cn/projects/arm-mobile-studioxing-neng-you-hua-yi url
前面的图介绍了每个 Shader Core 中的结构,那么手机 GPU 中一般会有多核核心,也就是 多个 Shader Core。我手上的这台 Oppo R15 使用的是 Mali-G72 GPU,ARM 提供了 1-32 个 核心的自由拓展,联发科的这个 GPU 只拓展了 3 个核心。
如下图可以看出每个 Shader Core 都共享了 L2 缓存,缓存的大小在 64-128KB 之间也可以 由芯片生产商自由拓展,另外厂商还可以拓展储器的存储器端口的数量和总线宽度。

Valhall 架构可以每个时钟每个内核写入两个 32 位像素,这样如果配备 4 个核心每个时钟周 期的读取和写入操作共 32X2X4=256 内存带宽。 Streamline 的启动可以参考我的上一篇文章,为了数据的更加精准,这里需要选择你测试的 手机,我这里选择 G72-Bifrost。

Cortex-A53,这里是用的 4 个小核,通过我实际测试小核用的几率非常小。

Cortex-A73,这里是用的 4 个大核,Unity 项目实际会一直使用大核。通过左侧的 User Activity 和 System Activity 可以看出每个大核的的压力,可以看出系统并没有占用,而是我们自己的 游戏占用的比较大。

接着就是小核和大核的时间周期,比如手机的主频 2GHZ 说的就是 1 秒内可执行的周期数。 Cycles/CPU 的占比 就能算出来一帧每个核处理 CPU 最大的周期数,也可以调整单位周期 为 1 秒,这样看起来会更清楚。

采样的时间单位是可以调整的,默认为 100 毫秒,这里可以改成 1 秒。

Streamline 还提供了统计 CPU 耗时函数的功能,在 Unity 打包的时候需要保存 Il2cpp 带符 号表的 so,默认生成 so 名字是 libil2cpp.dbg.so 主要一定要修改成文件名成 libil2cpp.so 不然 Streamline 无法解析,这个坑文档上没有写,我是一点点试出来的。

然后将 libil2cpp.so 添加就可以统计 C#的耗时函数了


我们先来测一个 Unity 的 Demo OPPO R15 联发科 Helio P60 的性能

在看看小米 10pro 高通 865 的性能

Helio P60 的 CPU 并没有吃满,而高通 865 的 CPU 大核(最后一个 2841HZ)已经完全吃 满,高通 865 的功率已经高达 7.6W,所以它只能玩 2.3 的小时就没电了。通过这个例子我 想告诉大家的是,帧率低并不一定代表费电,也可能是它 CPU 或者 GPU 根本跑不动,真正引起费电发热的是功率。
获取手机电流、电压、功率的方法可以参考我的这篇文章 Unity3D研究院之实时获取手机电流、电压、计算功率发热(一百一十八)https://www.xuanyusong.com/archives/4753 url
获取CPU和GPU温度,手机不需要Root,只有高通的芯片支持,代码如下。


而且还有降频 Helio P60 的帧率只有 6 但是从头到尾它都没有降频,我们没办法准确的知道 CPU 或 GPU 合适降频,但是可以通过它的实时电流算出当前是否降频。 接着就是 GPU 使用部分

GPU Active:就是 1 个时间单位帧内 GPU 的所使用的时钟周期。如下图所示,我们可以修 改采样的时间,我们修改成 1 秒。

这样我们就能看到 1 秒钟 GPU 当前所用的周期是 800MHZ 左右,那么当前手机使用的是 G72MP3 官方给的频率是 850MHZ,显然目前 GPU 已经是满负荷状态。 在来看看 GPU 的工作原理,Job Manager 负责将工作交给 Non-fragment(非片元着色的工 作)和 Fragment(片元着色的工作),理想状态下它们可以与 CPU 的工作同时进行,而且 Non-fragment 和 Fragment 也可以并行进行。
Non-fragmentqueueactive 非片元队列活动周期,队列包含顶点着色器、曲面细分着色器、 几何着色器、Tile 分块以及计算着色器。
Fragment queue active 片元队列活动周期,一般手游中片元着色压力是非常大的,上图中 可以看到一共 792M 都用于片元,片元着色和上面的非片元着色是并行处理的。
Tileractive 平铺周期,它主要处理分块,TBR的架构顶点着色完需要先给内存写入分块List, 然后在片元着色在读入分块,只要软件这边控制好顶点和片元着色,这里不应该会产生瓶颈。
Interruptactive 中断,和Tileractive一样,软件这边控制好顶点和片元着色,这里不应该会 产生瓶颈。
这些参数我们和下图中是一一对应,在通过上图我们还是能看出来,目前 GPU 卡在了片元 着色上。

每帧会处理两个渲染通道,首先执行 Not-fragment,结束后向 CPU 触发一个中断 Interrupt, 并且执行第二个 Not-fragment,等第一个 CPU 处理完中断后,接着 Fragment 继续执行如 此往复。

前面我们已经能得到 GPU 具体使用了多少个周期,但是知道多少个周期并不能马上算出是 否是瓶颈,比如上图中我们知道片元基本上是卡死了,但是顶点着色并没有完全卡死,到底 顶点着色占用了总性能的百分比是多少呢?
GPU 使用情况以及利用率 通过对比可以看出片元着色占用 GPU 总性能的 99% 而顶点只占用了 1.9%,看到这组数据就 明确我们要想办法优化片元着色。

显存带宽 GPU 在读写内存就需要占用带宽了,比如在 shader 中写深度、shadow map 等都需要给内 存写入,shader 中采样贴图则需要从内存中读取,无论写还是读都需要占用内存带宽。带宽 如果过高,则会导致性能问题,而且最重要还是费电。
现在的 GPU 都采用 TBR/TBDR 分块的方案最终的目的就是省电,要说 TBR/TBDR 就一定要 说 IMR 也就是之前桌面 GPU 普遍采用的方法,但是现在很多桌面的 GPU 也采用 TBR 切块 的方案了。 IMR 的方案,遍历每个 Render Pass,在遍历每个图元执行顶点 shader,在遍历每个图元执 行片元 shader。

这样带来的问题就是在片元 Shader 中需要频繁对内存进行读取/写入,它在移动端就行不 通,注意下图中 FragmentBuffer WorkSet 这里就是在频繁的读写内存,这样就一直占着带宽 的读写就会非常费电.

为了减少费电就避免占用带宽,避免对内存进行频繁的写入/读取操作,所以就诞生了 Tile Base Render(TBR)的方案,Mali 会把整个屏幕分成若干个 16X16 个 Tile 块。
先遍历 Render Pass 在遍历图元,执行顶点着色器并且将 Tile 信息保存到 TileList 中存入内 存,注意这里会发生一次内存写入操作。

接着片元着色器会从显存中读取顶点 Shader 存入的 Tilelist 信息,这里会发生一次内存读取 操作。然后就是遍历每个图元执行片元着色器。

每个片元着色后并不直接写入显存,而且直接写入 GPU 中的 Local Tile Memory 中,因为这块内存就在 GPU 中并不需要占带宽,读写它不仅速度快而且也会省电。但是成本高所以它 的容量是有限的,最后统一将 Frame Buffer 写入显存中,这时会发生一次写的操作。

通过对 TBR 原理的掌握我们应该明白减少带宽是非常重要的。 现在手机中使用的内存是 Low-Power DDR SDRAM 缩写 LPDDR,通过名字就能看出它是一 种低能耗的随机存储器。我的测试机 OPPO R15 采用的是 LPDDR4X,在官网中可以查到它 的频率是 2133MHZ
带宽上限 = 内存频率 位宽 2133MHZ 16 = 34G/s 可以算出 OPPO R15 的带宽大概 理论是 34G/S (如果需要换算成字节还需要除以 8 ,由于 streamline 也是按位来的,所以 就不除以 8 了)
如下图可以看出我们的带宽读写加起来大概 3.1G/S,ARM 给的建议是移动手机上 5G/S 以 内就是安全的,结合我们实际游戏看看也可以小于 5G/S 左右

因为访问内存是比较慢的,所以 ARM 还提供了一个 L2 的缓存,如果缓存命中率高就会减 少带宽提高性能。

Mali Memory Stall Rate 内存停顿率 高的停顿率表示内容所请求的数据量超出了内存系统所能提供的数量,所以说优化的策略 还是减少带宽。

Mali Memory Stall Rate 内存读取延迟周期 文档中表示超过 256 个 cycles 就表示请求的数据量超出了内存系统所能提供了,这时就需 要优化带宽了。

接着就是几何的使用和裁减了,说这块数据前我们必须学习一下 GPU 的流水线。

- Position Shading 这个最开始讲过,从第三代 Bifrost 架构开始支持了 IDVS,这里先处理 只包含的顶点坐标。
- Facing Test Culling 剔除掉背面的多边形。
- Frustum Test Culling 剔除掉 frustum 截面体以外的多边形,也就是剔除摄像机看不到 (注意摄像机并不是真实存在的,所以 frustum 更加贴切)
- Sample Test Culling 剔除掉非常小小到连 1 个像素都没有的多边形。
- Varying shading 如果几何体没有被之前的剔除掉,开始执行顶点的其他着色计算。
- Polygon List 生成多边形列表,也就是前面提到的 tilelist 写入显存中,这里会发生一次内存写入。

- 片元上 GPU 要先从内存中取出多边形的 Tile list,这里会发生一次内存读取。
- 开始进行光栅化,将多线形变成 2D
- 开始进行 Early ZS Test,这个概念非常重要,这时 2D 的几何体是有深度的,GPU 就知 道如何进行排序,这样就可以将完全挡住的像素删掉,后面的就不会在进行。但是如果 你的 shader 中用了 Alpha Test 的 discard clip 这种操作,此时 GPU 就无法相信光栅化后 的排序,这个像素对应的后面的计算就需要执行,那么性能就差了。
- 创建 fragment 线程开始执行片元着色器。
- Late ZS Test,片元着色器都执行完后,这时候就能真正确认每个像素的遮挡关系,继续 剔除掉被挡住的像素。但是注意 fragment 压力是非常大的,所以一定要优先支持被 Early Z 剔除掉。
- Blender,开始混合颜色,也就是上色。
- Tile RAM & Tile Write 将颜色存入每个 Tile 块中,此时还在 GPU 中,所以这里不会占用带宽。
- Transaction Elimination,这也是个非常重要的功能,比如你站在游戏中什么也不动,这时候前后 2 帧其实有大面积的像素是不需要刷新的,所以 GPU 会给每个Tile 块中的所有颜色计算出一个唯一 类似hash 的东西和并且和现在的 Frame Buffer 做比较,当发现块中颜色真正发生改变才将这个块写入显存中,所以这样可以进一步减少带宽。
Mail Geometry Usage:几何体使用 Visible Primitives:最终显示的几何图元 Culled Primitives:被裁掉的几何图元 Input Primitives:输入的几何图元 如果被裁掉的几何图元过多,那么就需要考虑项目 LOD 的方案了。

Mail Geometry Usage:几何体的裁切率 几何剔除需要一次进行这 3 部分剔除,前面已经讲过这里就不再赘述。

这里详细记录每一步剔除率,总的来说如果数据比较夸张,那就一定要做好 LOD 的方案。

还记的前面提到的 IDVS 吗?GPU 将顶点着色器分成了两部分,一个是专门处理位置的着色 器,另一个是处理其他的着色器,这里就是处理的线程数量。

像素 这个值我觉得非常有意义,比如现在测试机 GPU 是 850MHZ 共 3 核心,分辨率是 2280X1080, 如果目标帧率时 60 的话,我们就能算出每秒能处理的最多像素数 (850MHZ X3)/(2280X1080 X 60) = 17 也就是说或是这台手机要想流畅运行 60 帧的话,留给每个像素的 GPU 时钟周期必须小于 17,当然这还包括前面处理顶点的时间周期。下图中我们的采样周期是 1 秒,也就是说 1 秒 产生了 375M 像素。

过度绘制 前面提到了 1 秒产生了 375M 个像素,理想情况下每个像素只绘制一次,但实时上是不可能的,比如 Alpha blend 这种会在同一个像素上 Blend 多次,这就是我们说的 OverDraw,实 际 Fragment 的数量除以最终产生的像素数,这里得到了 603M/375M = 1.6 大概每个像素被过度绘制 1.6 倍。

像素吞吐量 使用 GPU 实际执行的周期数除以像素数,这样就能算出像素的吞吐量了。

Early ZS Rate 我们要尽可能的让像素被提前 Early Z 裁减掉,这样就可以避免执行片元着色优化效率,这 里就可以看到真正被 Early Z 裁减掉的比例,不透明物体一定要从前往后绘制,这样大部分 都可以被 Early Z 掉。 Early ZS test rate :参与 Early Z 深度和模板测试的比例。 Early ZS update rate: Early z 深度和模板测试期间更新帧缓冲区的光栅化四边形的百分比 Early ZS Kill rate: 被 Early z 深度和模板测试干掉的光栅化四边形的百分比 FPK kill rate: Forward Pixel Kill (FPK)的缩写,如果被前面的像素挡住将终止这个线程,后面 也不会将不透明的像素写入相同像素中。

然后就是 Late Z 了,比如用了 Alpha Test,可能就能在这里被剔除掉,但是到这里实际上片 元着色已经走完,优先要让被 Early Z 掉才最好。

着色器核心路径

注意就是下图中 Non-fragment warps 和 Fragment warps 这两部分的数据量。

GPU 核心的吞吐量,周期数除以线程数。

GPU 核心利用率

GPU 核心单元利用率 尽量减少每个单元的利用率有助于提高性能。

GPU 每个核心会分这几个单元,下图定义了每个节点都干了些什么。

GPU 负载率属性 Partial coverage rate:部分覆盖率,如果过高说明有面数过高的三角形,需要做 LOD Diverged instruction rate:差异指令率,意思就是控制差异指令的百分比(我也没看懂) Constant tile kill rate : Tile 块被杀掉率,如果比例较高,说明多帧画面几乎显示是一样的, Tile 块没发生改变是不会刷新显存,只助于节省带宽。

顶点变化使用情况 前面我们提到顶点着色器会被分成两部分,一部分是处理坐标的着色计算,另一部分就是除 坐标以外需要处理的着色计算。走到这里说明三角面没有被裁减掉,接着就需要做插着计算 了。 Varying active :当前需要处理的差值周期。 16/32 bit interpolation active :处理 16 位/32 位差值周期。

纹理单元使用情况 记录纹理采样和纹理过滤的周期。 采样一张图,从多平面 YUV 采样每个平面需要一个周期。 2D 双线性过滤需要一个周期。 2D 三线性过滤需要两个周期。 3D 双线性过滤需要两个周期。 3D 三线性过滤需要四个周期。

平均每个纹理过滤产生的周期数

纹理单元工作负载属性 Compressed access rate :压缩纹理占总纹理的百分比 Mipmapped sample rate :访问 mipmap 占总纹理的百分比 Trilinear filtered sample rate:三线性过滤占总纹理的百分比 3D access rate:3D 纹理占总纹理百分比

纹理内存使用情况 L2 read bytes/cy: 从 L2 缓存中读取的纹理字节 External read bytes/cy 从内存中读取的纹理字节
优化思路如下 启用 Mipmap 使用 ASTC 或 ETC 压缩格式 用较小的格式替换运行时生成的帧缓冲区和纹理格式 减少使用任何用于纹理锐化的负 LOD 偏移(没看懂,可能是 Mipmap 的 lod 偏移吧) 降低 MAX_ANISOTROPY 级别以进行各向异性过滤(Unity 里贴图的 aniso level 使用默认的 1 就行,越大越占性能)

加载/储存单元 负责内存读取与写入,这里不包括贴图采样和帧缓冲写回操作。

加载/储存单元的内存情况

L2 缓存和内存的读写情况。

Streamline 的参数都介绍完毕了,大部分都能理解,有些理解起来确实比较困难,接下来就 进入优化环节。
- 片元 Bound 如果游戏性能出了卡顿发热的问题,大概率问题都出现片元上,优化的方向大概有 4 点。 a) 过度绘制 overdraw,不透明物体一定要从前往后绘制,由于透明物体必须从后往前画所 以无法避免 overdraw,能做的就是减少透明物体。 b) 分辨率太高,现在的手机基本都超过了 1080P 的分辨率,其实很多 PC 主机用的分辨率 也就是 1080 还不如手机分辨率高,但是手机屏幕小啊,所以要适当降低分辨率。 c) 降低纹理带宽,Shader 中采样的贴图过多就可能造成带宽卡住,这样就会卡住无法后续 shader 计算。 d) Shader 复杂,GPU 指令多,比如 Nexus 10 原始分辨率= 2560 x 1600 = 4,096,000 像素, 四核 GPU 533MHz≈520 个周期/像素,如果目标帧率 30 FPS 留个每个像素的 GPU 周 期就是 17 ,大家可以粗略计算一下自己游戏。
- 顶点 Bound 就是顶点数据过多,一般手游都不会 Bound 在顶点上。但是顶点数过多会给片元带来更 大压力,所以一般我们会要求美术做的 2D 模型不能超过多少多少面,就是为了减少顶点数 量。
那做圆形来说左 1 和左 2 性能都比右 1 差,美术同学可能知道在减面的时候使用右 1 的方案,但其实右 1 还能优化 GPU 的性能。原因就是 GPU 是 2X2 一次执行 4 个像素,如果 不巧这 4 个像素遇到三角形与其他三角形的边缘,比如左 1 的中心点,那么四边形就会被 处理 N 次,性能就严重浪费。

上图中 3 个圆形的顶点数还是比较少性能差距不太明显,如果顶点数多了那么就能看出很 大的性能差距。有人做了个测试,如下图所示,横坐标是三角形数量,纵坐标是帧率。明显 能看出性能的差距。

- GPU 带宽 Bound 手机带宽总带宽的计算方式前面我们已经介绍过,这里就不赘述了,请大家严格限制带 宽,最快的方法就是使用压缩贴图,比如 ARM 推出的 ASTC 压缩格式。
- CPU Bound CPU 卡住的手段就很多了,比如 Unity 的 Profiler 就是一种方案,当然也可以使用前面介绍 streamline 的方法。
参考:https://developer.arm.com/ip-products/graphics-and-multimedia/mali-gpus/mali-performance-counters/mali-g72-counters
发布于技术交流
4条评论

问
AI
全新AI功能上线
1. 基于Unity微调:专为Unity优化,提供精准高效的支持。
2. 深度集成:内置于团结引擎,随时查阅与学习。
3. 多功能支持:全面解决技术问题与学习需求。

问
AI