共计 36495 个字符,预计需要花费 92 分钟才能阅读完成。
导语 | 本文简述了 GPU 的渲染管线和硬件架构,对一些常见问题进行了探讨和剖析。有以下几点核心内容:(1)挪动平台渲染管线 TBDR 的介绍;(2)GPU 缓存体系的介绍;(3)Warp 的执行机制;(4)常见的如 AlphaTest 或者分支对性能的影响。
GPU 渲染管线
一、渲染管线简述
所谓渲染管线,就是 CPU 传送给 GPU 一堆数据(顶点、纹理等),通过一系列解决,最初渲染得进去一副二维图像。有以下几个阶段。
- 应用程序阶段
1. 粗粒度剔除、渲染状态设置、筹备数据。
2. 咱们在游戏引擎中所做的视锥剔除、遮挡剔除等,都是粗粒度剔除,是基于模型级别的。
3. 这一步是在 CPU 进行的,前面的步骤都是在 GPU 外部进行的。
- 顶点解决阶段
1. 顶点着色器、曲面细分、几何着色器、顶点裁剪、屏幕映射。
2. 这里会做反面剔除等裁剪,确保只有真正须要绘制的图元才会进入光栅化。
3. 顶点解决是可编程的(Vertex Shader,Geometry Shader 和 Compute Shader)。
- 光栅化阶段
1. 三角形设置、三角形遍历、片元着色器。
2. 光栅化引擎会将图元 (Primitive) 映射为与屏幕像素对应的片元(Fragment)。片元蕴含每个像素的坐标、色彩、深度、法线、导数、纹理坐标等信息。这个数据通过片元着色器的计算失去最终的色彩值。
3. 像素解决是可编程的(OpenGL 中叫做片元着色器,Fragment Shader,DirectX 中叫做像素着色器,Pixel Shader)。这里通常是性能瓶颈所在,所以古代 GPU 做了很多的优化来尽可能防止执行有效的像素解决,比方 EarlyZ、隐面剔除等。
- 逐片元操作
1. 裁剪测试、深度测试、模板测试、混合。
2. 光栅化阶段失去的色彩值通过一些列的测试、混合,最终写入到 FrameBuffer 中。
3.ROP (Raster Operations),是由一个独立的硬件单元来实现的。这个硬件单元的数量和性能限度了 GPU 每秒写入 FrameBuffer 的数据量。在一些低端机这个阶段也十分有可能成为性能瓶颈,即每秒 ROP 解决数据量。即使应用最简略的全屏半透明 shader,多叠加一两层就会因 overdraw 而重大降帧,很有可能就是 ROP 瓶颈。
二、IMR: Immediate Mode Rendering
上图展现了一个十分经典的 IMR 渲染管线,也是桌面端最常见的 GPU 架构。GPU 会在每个 drawcall 提交中,依照管线的程序解决每个图元。
-
劣势:
1. 其劣势是渲染管线没有中断,有利于进步 GPU 的最大吞吐量,最大化的利用 GPU 性能。同时从 vertex 到 raster 的解决都是在 GPU 外部的 on-chip buffer 上进行的,这意味着只须要很少的带宽 (bandwidth),就能够存取(storing and retrieving) 处理过程中的图元数据。
2. 所以桌面 GPU 人造就能够解决大量的 DrawCall 和海量的顶点。而挪动端 GPU 则对这两者异样敏感。这不仅仅是 GPU 性能差别,架构差别也至关重要。
-
劣势:
IMR 是全屏绘制的。以后绘制的图元可能存在于屏幕的任何地位。这意味着咱们须要一个全屏的 FrameBuffer。这个 buffer 的内存很大(比方 30MB,大小跟屏幕分辨率和渲染指标的格局无关),所以只能放在零碎内存 (DRAM) 中。而在光栅化之后的 fragment shading, depth testing, stencil testing, blending 等阶段,都会大量的与零碎内存进行交互,耗费大量的带宽。GPU 的 L1/L2 缓存能够缓解这个问题,然而对于挪动端,仍然是不可承受的开销。
三、TBR: Tile-based Rendering
-
为什么要应用 TBR 架构
1. 对挪动端设施而言,管制功耗是十分重要的。功耗高意味着耗电、发热、降频,这会导致咱们的游戏呈现重大的卡顿或者帧率升高。带宽是功耗的第一杀手,大量的带宽开销会带来显著的耗电和发热。
2. 挪动端 GPU 的带宽原本就跟桌面端 GPU 不是一个量级,又无奈像独立显卡一样独占大量带宽,所以缩小带宽开销变得异样重要。因而挪动端 GPU 广泛应用的是 TBR/TBDR 架构。
-
TBR 架构的原理
TBR 跟 IMR 不同之处在于,它并不是基于全屏间接绘制。而是把屏幕分成一个一个的 Tile,GPU 一次只绘制一个 Tile。绘制结束再将绘制后果写入到零碎内存的 FrameBuffer 中。
TBR 架构的绘制过程分成两个局部
第一步,解决所有顶点,生成一个 tile list 的两头数据(FrameData)。这个数据保留了每个图元归属于哪个屏幕上的 Tile。PowerVR 一个 Tile 是 32×32 大小,而 Mali 则是 16×16 大小(行将公布的 Mali-G710 也批改为 32×32 大小了)。
第二步,针对每个 Tile 执行像素处理过程(光栅化、片元着色器等),每个 Tile 处理完毕,将后果一起写入到零碎内存中。
-
劣势
1.TBR 架构最大的劣势就是缩小了对主存的拜访,也即缩小了带宽开销。每个 Tile 足够小,其 framebuffer 是能够做到 on-chip memory 上的。on-chip memory 紧挨着 GPU 的 shader core,访问速度极快。
2. 不仅仅是 fragment shader,depth testing、blending 等操作也是在 on-chip memory 进行的。大幅缩小像素解决阶段对系统内存的拜访。
3. 还有一个显著劣势是,depth buffer 和 stencil buffer 只在 Tile 解决外部有用。它们是不须要写回零碎内存的,这进一步节俭了带宽开销。
4.TBR 架构里,一些本来十分低廉的,耗费大量的带宽的算法,会变得高效起来。比方 MSAA (Multi-Sample Anti-Aliasing)。
-
劣势
1.GPU 要解决所有的顶点,生成 tile list,而后才能够进行光栅化。跟 IMR 相比,这里会有显著的“提早(latency)”。
2. 生成的这个 tile list 数据是要存到零碎内存中的。这同样会有带宽开销。顶点越多,计算的压力就越大,带宽耗费也会越多。像曲面细分(tessellation),在 TBR 架构下就异样的低廉。
3. 所以挪动端游戏对顶点数量更加敏感。如果顶点数量过大的话,会导致性能重大降落。
四、TBDR: Tile-Based Deferred Rendering
TBDR 和 TBR 模式根本相似,惟一的区别在于,多了一个 隐面剔除(Hidden Surface Removal) 的过程。就是上图中 HSR & Depth Test 这个步骤。通过 HSR,无论以什么程序提交 drawcall,最终只有对屏幕产生奉献的像素会执行像素着色器。被遮挡的片元会被间接抛弃掉。
不同的 GPU 有本人的隐面剔除技术,比方 PowerVR 就是 Hidden Surface Removal (HSR),Adreno 就是 Low Resolution Z (LRZ),Mali 就是 Forward Pixel Kill (FPK)。其原理和实现各不相同,不过最终目标都是为防止执行有效的像素着色器。
五、总结
IMR 是桌面端 GPU 的支流架构。NVIDIA 较新的显卡也局部反对了 Tile based 的个性。不过这个 Tile 是较大的 Tile,而不是像 Mali 芯片这样 16×16 的小 Tile。
TBR 是挪动端 GPU 的支流架构,通过拆分成一个个 Tile 绘制,缩小与主存的交互,进而缩小带宽开销。
TBDR 一开始专指 PowerVR,其光栅化之后并不是立刻渲染,而是多了隐面剔除的过程。起初 Adreno 和 Mali 也别离提出了本人的隐面剔除计划。所以认为当初的挪动端 GPU 都是 TBDR 架构也不为过。
举荐浏览 GPU Framebuffer Memory: Understanding Tiling 这篇文章,更加直观的理解不同架构的绘制过程。
GPU 硬件架构
一、GPU 和 CPU 的差别
这张图展现了 CPU 和 GPU 的硬件差别。
CPU 外围数量少(计算单元少),每个外围都有管制单元。内存设计上是大缓存、低提早。
而 GPU 则恰好相反,计算单元十分多,多个计算单元共享一个管制单元。内存设计上谋求高带宽,能够承受较高提早。
所以 CPU 中司空见惯的分支管制,逻辑运算,在 GPU 中成了奢侈品。而面对海量数据并发计算的场景,GPU 则比 CPU 快好几个数量级。
CPU 和 GPU 的差别能够形容在上面表格中:
二、CPU 的缓存体系和指令执行过程
尽管本文次要讲的是 GPU 架构,不过 CPU 和 GPU 有很多中央是相通的,同时又有一些设计方向是相同的。理解 CPU 能够帮忙咱们更好的了解 GPU 的执行过程。
-
内存的硬件类型
1.SRAM(Static Random Access Memory,动态随机存取内存)具备静止存取数据的作用,然而断电后数据会隐没,不须要刷新电路就可能保留数据,速度较 DRAM 快很多。个别用作片内缓存(On-chip Cache),例如 L1 Cache、L2 Cache。
2.DRAM(Dynamic Random Access Memory,动静随机存取内存)须要不停地刷新电路,否则外部的数据将会隐没,因而被称为“动静”存储器。罕用于内存,容量较 SRAM 大。个别用作零碎内存(System Memory)。当初桌面端的内存都是 DDR(DDR SDRAM,Double Data Rate Synchronous Dynamic Random-Access Memory,简称 DDR)。
3.GDDR(Graphic DDR),用作显存。时钟频率更高,耗电量更少。晚期显存也是应用 DDR 的,不过前面独立倒退为 GDDR。DDR 当初还处于 DDR4 规范,而 GDDR 曾经倒退到 GDDR6 了。
4.LPDDR SDRAM,简称 LPDDR(Low Power Double Data Rate)。是挪动设施罕用的一种低功耗 SDRAM,以低功耗和小体积著称。FrameBuffer 便寄存于此。芯片参数中说的 LPDDR4/LPDDR5,说的便是这个,代表了零碎内存的性能。
5.UFS(Universal Flash Storage)。通用闪存存储,如 ufs2.1/ufs3.1 等。代表了挪动设施“磁盘”的性能。
-
CPU 的缓存体系
CPU 的缓存有 L1/L2/L3 三级缓存。L1 缓存和 L2 缓存是在 CPU 外围外部的(每个外围都配有独立的 L1/L2 缓存),L3 缓存是所有外围共享的。缓存是 SRAM,速度比零碎内存(DRAM)要快十分多。
L1/L2 缓存是片上缓存,速度很快,然而通常比拟小。比方 L1 cache 通常在 32KB~256KB 这个级别。而 L3 cache 能够达到 8MB~32MB 这个级别。像苹果的 M1 芯片(CPU 和 GPU 等单元在一个硬件上,SoC),L3 缓存是给所有硬件单元应用的,所以也被称为 System Level Cache。
L1 缓存分为指令缓存(I-Cache)和数据缓存(D-Cache),CPU 针对指令和数据有不同的缓存策略。
L1 缓存不可能设计的很大。因为增大 L1 缓存尽管会缩小 L1 cache missing,然而会减少拜访的时钟周期,也就是说升高了 L1 cache 的性能。
CPU 的 L1/L2 缓存须要解决缓存一致性问题。即不同外围之间的 L1 缓存之间的数据应该是统一的。当一个外围的 L1 中的数据发生变化,其余外围的 L1 中的相应数据须要标记有效。而 GPU 的缓存不须要解决这个问题。
CPU 查找数据的时候依照 L1–>L2–>L3–>DRAM 的程序进行。当数据不在缓存中时,须要从主存中加载,就会有很大的提早。
缓存对进步 CPU 的执行性能有着十分重要的意义。如下面的 Intel i7 die shot 所示,很多时候缓存会占据芯片中一半以上的晶体管和面积。苹果的 A14/A15/M1 芯片性能上碾压同档次的其余 SoC,跟超大缓存密不可分,其 L2/L3 缓存个别是其余 SoC 的两三倍。
-
CPU 指令的执行过程
经典的指令流水线有上面五个阶段
1.Instruction Fetch,取指令。从指令缓存(I-Cache)中取出指令,放在指令寄存器中。
2.Instruction Decode,指令解码。这里还会通过寄存器文件(Register File)取到指令的源操作数。
3.Execute,执行指令。
4.Memory Access,如果须要存储器取数据(load/store 指令),则通过数据缓存(D-Cache)取数据。不拜访存储器的指令此阶段不做任何事件。
5.Register Write Back,将指令执行后果写入目标寄存器。
拜访主存,无论是 GPU 还是 CPU 都会呈现较大的提早。面对提早,CPU 和 GPU 的解决方案不一样。CPU 是通过大容量高速缓存,分支预测,乱序执行(Out-of-Order)等伎俩来遮掩提早。CPU 缓存容量比 GPU 要大,比方 GPU 就没有 L3 缓存。而且 CPU 的缓存是以低提早为指标设计的。GPU 是另外一种思路,通过切换线程 Warp 来躲避指令提早带来处理单元的进展,下文介绍 GPU 的 Warp 机制的时候还会提到这一点。
如果呈现分支,CPU 是通过分支预测,来进步流水线的执行性能。古代 CPU 的分支预测准确率很高,能够达到 90% 以上。当然一些蹩脚的代码影响了分支预测,就会呈现性能问题。GPU 没有分支预测单元,所以并不善于执行分支。
CPU 能够同时发射多条指令,让指令可能并行计算,而不是一条流水线串行计算,这样能够更好的利用计算单元。这个就是超标量设计(Super Scalar)。古代 CPU 根本都是超标量的。
如果依照本来的指令程序执行,可能指令之间有依赖无奈并行执行,或者频繁呈现高提早指令。所以 CPU 会在保障执行后果正确性的根底上,批改指令的执行程序,让指令可能更加高效的执行,从而缩小执行期待,晋升管线性能。这个就是乱序执行。
CPU 的线程切换会有显著的上下文(Context)切换开销。因为切换到其余线程须要将寄存器和程序计数器保存起来,等切换回来的时候还须要复原寄存器和程序计数器。所以 CPU 会尽可能防止频繁的线程切换。而 GPU 因为寄存器数量很多,线程切换时不须要保留上下文,所以就能够通过零老本的切换线程来遮掩提早。
Intel CPU 也有应用 GPU 的思路,筹备两套寄存器,CPU 外围在两个线程之间切换的老本就非常低。这样一个外围就能够当做两个外围来应用。这就是超线程技术。不过 CPU 毕竟不像 GPU 有大量寄存器,外围在两个线程之间切换,并不一定可能保障升高提早,同时不能精确管制每个线程的执行工夫。所以很多游戏以及高性能计算程序都是敞开超线程的。
三、GPU 渲染过程
具体渲染过程,其实就是经典的渲染管线的执行过程。能够跟上一部分的渲染管线流程图对照浏览。举荐浏览 Life of a triangle – NVIDIA’s logical pipeline 一文。
应用程序通过图形 API(DirectX、OpenGL、Vulkan、Metal)收回渲染命令,通过驱动传输数据给 GPU。GPU 通过主机接口(Host Interface)承受这些命令,并通过前端(Front End)解决这些命令。
SM (Streaming Multiprocessor) 负责解决执行顶点着色器。古代 GPU 都是对立着色器架构(Unified Shader Architecture),顶点着色器和像素着色器应用雷同的解决外围执行。这样 GPU 能够更好的做负载平衡,以适应顶点工作重或者像素工作重的不同工作情景。
解决过的三角形会被裁剪,而后调配给光栅化引擎。在光栅化阶段,会把三角形离散为与屏幕对应的栅格信息。
光栅化后的片元,通过 EarlyZ 剔除,生成像素线程。32 个线程为一个线程束(Warp)。这是 GPU 计算外围的最小工作单元。
接下来是在 SM 中执行像素着色器。一个 Warp 中执行的指令是一样的,然而数据不一样(SIMD/SIMT)。
执行完像素着色器之后,数据会交给 ROP (Raster Operations)。因为像素着色器执行有快有慢,所以这里会有排序过程,保障执行 ROP 的程序和原始 API 的调用程序是统一的。一个 ROP 外部有很多 ROP 单元,在 ROP 单元中解决深度测试,和 Framebuffer 的混合。深度测试和色彩写入必须是原子操作,否则两个不同的三角形在同一个像素点就有可能会有抵触和谬误。
四、桌面端 GPU 硬件架构
上图展现的是 NVIDIA Fermi 架构的示意图。
不同的 GPU,架构差别较大,然而大体都蕴含下列外围组件:
1.SM、SMX、SMM (Streaming Multiprocessor)。GPU 的外围,执行 Shader 指令的中央。一个 GPU 由多个 SM 形成。Mali 中相似的单元叫做 Shader Core,PowerVR 中叫做 Unified Shading Cluster (USC)。
2.Core。真正执行指令的中央。NVIDIA 叫做 CUDA Core。Mali 中叫做 Execution Engine 或者 Execution Core。PowerVR 中叫做 Pipeline。当然因为硬件构造差别,Execution Engine、Pipeline 和 CUDA Core 并不等价。前面还会对此再做剖析。
3.Raster Engine。光栅化引擎。
4.ROP (Raster Operations)。depth testing, blending 等操作都是在这里实现的。
5.Register File, L1 Cache, L2 Cache。寄存器和各级缓存。
五、Shader Core 的次要形成单元
1.32 个运算外围(CUDA Core,也叫流处理器 Stream Processor)
2.16 个 LD/ST(Load/Store)模块来加载和存储数据
3.4 个 SFU(Special function units)执行非凡数学运算(sin、cos、log 等)
4.128KB 寄存器(Register File)3 万个 32-bit 的寄存器,大寄存器设计
5.64KB L1 缓存(On-Chip memory)
6. 纹理读取单元(Texture Unit)
7. 指令缓存(Instruction Cache)
8.Warp Schedulers:这个模块负责 warp 调度,一个 warp 由 32 个线程组成,warp 调度器的指令通过 Dispatch Unit 送到 Core 执行。
六、GPU 的内存构造
-
UMA(Unified Memory Architeture)
这张图展现了桌面端和挪动端的内存构造差别。
桌面端独立显卡是右边的分离式架构,CPU 和 GPU 应用独立的物理内存。而挪动端是右侧的对立内存架构,CPU 和 GPU 共用一个物理内存。桌面端的集成显卡也是 UMA 架构。
UMA 并不是说 CPU 和 GPU 的内存就在一起了,实际上它们所应用的内存区域并不一样。物理内存中有一块儿专有区域由 GPU 本人治理。CPU 到 GPU 的数据通信仍然是有拷贝过程的。如果广义上说,像主机平台或者苹果 M1 芯片这样能够实现 CPU 和 GPU 的零拷贝数据传输的架构才是真正的 UMA,挪动端这种架构只能算是共享物理内存。
挪动芯片都是 SoC(System on Chip),CPU 和 GPU 等元件都在一个芯片上,芯片面积(die size)寸土寸金。天然不可能像桌面端一样给显卡装备 GDDR 显存,通过独立的北桥(PCI-e)进行通信。在挪动端 CPU 和 GPU 应用同一个物理内存也更加灵便一些,操作系统能够决定调配给 GPU 的显存大小。当然副作用就是 CPU 和 GPU 很多时候会抢占带宽,这会进一步限度 GPU 能应用的带宽。
GPU 应用独立的显存空间的益处是,GPU 能够对 Buffer 或者 Texture 做进一步优化,比方对 GPU 更加敌对的内存排布。显存中存储的数据可能并不是咱们理论 Upload 的数据。所以即使在手机上,CPU 和 GPU 共用的是一块儿物理内存,咱们仍然须要通过 MapBuffer 的模式来实现数据的拷贝。反过来说,如果 CPU 和 GPU 间接应用雷同的数据,那么 GPU 就无奈对数据做优化,可能会升高性能。
CUDA 前面有推出对立虚拟地址(Unified Virtual Address,UVA)和对立内存(Unified Memory,UM)的技术,将内存和显存的虚拟地址对立。不过这个跟物理内存是合并的还是拆散的没有关系。其目标是为了减化开发者写 CUDA 程序的内存治理的累赘。
- GPU 缓存的分类
GPU 缓存构造
L1 缓存是片上缓存(On-Chip),每个 Shader 外围都有独立的 L1 缓存,访问速度很快。挪动 GPU 还会有 TileMemory,也就是片上内存(On-Chip Memory)。
L2 缓存是所有的 Shader 外围共享的。属于片外缓存,离 Shader 外围略远,所以访问速度较 L1 缓存要慢。
DRAM 是主存(零碎内存,能够叫做 System Memory,Global Memory 或 Device Memory),访问速度是最慢的。FrameBuffer 是放在主存上的。
内存访问速度
内存的存取速度从寄存器到零碎内存顺次变慢:
存储类型 | Register | Shared Memory | L1 Cache | L2 Cache | Texture/Const Memory | System Memory |
---|---|---|---|---|---|---|
拜访周期 | 1 | 1~32 | 1~32 | 32~64 | 400~600 | 400~600 |
寄存器访问速度是最快的,GPU 的寄存器数量很多。
Shared Memory 和 L1 Cache 是同一个硬件单元,Shared Memory 是能够由开发者管制的片上内存,而 L1 缓存是 GPU 管制的,开发者无法访问。局部挪动芯片如 Mali,是没有 Shared Memory 的,这个次要影响 OpenCL 开发。
Local Memory 和 Texture/Const Memory 都是主存上的一块儿内存区域,所以访问速度很慢。
NVIDIA 的内存分类
查资料的时候常常会看到这些概念,然而 NVIDIA 的内存分类是为 CUDA 开发服务的,与游戏开发或者挪动 GPU 还是有一些差别的。所以这里只须要简略理解即可。
1. 全局内存(Global memory)。主存,Device Memory。
2. 本地内存(Local memory)。Local Memory 是 Global Memory 中的一部分。是每个线程公有的。次要用于解决寄存器溢出(Register spilling,寄存器不够用了),或者超大的 uniform 数组。访问速度很慢。
3. 共享内存(Shared memory)。Shared Memory 是片上内存,访问速度很快。是一个 Shader 外围内的所有线程共享的。
4. 寄存器内存(Register memory)。访问速度最快。
5. 常量内存(Constant memory)。Constant Memory 和 Local Memory 相似,都是 Global Memory 中的一块儿区域,所以访问速度很慢。局部 GPU 会有专门的 Constant cache。
6. 纹理内存(Texture memory)。与 Constant Memory 相似,也在主存上。局部 GPU 有 Texture cache。
-
Cache line
1.GPU 和缓存之间的内存替换是以字节为单位,而是以 Cache line 为单位的。Cache line 是固定的大小,比方 CPU 的 Cache line 是 64 字节,GPU 是 128 字节。
2.Cache line 不仅仅是为了字节对齐。也有现实意义。想要晓得是否缓存命中,是否写入主存,必定要有标记位。所以一个 Cache line 就是标记位 + 地址偏移 + 理论数据。
3. 缓存命中与否性能差别微小。对一块儿内存进行程序拜访比随机拜访,性能可能要好很多。咱们纹理应用 Mipmap 能够是进步纹理的缓存命中率进而晋升性能。Unity 的 ECS 零碎也是冀望通过 Cache 敌对的数据布局来晋升性能。
-
Memory Bank 和 Bank Conflict
为了进步对内存的拜访性能,取得更高带宽,Shared Memory/L1 Cache 被设计为一个个的 Memory Bank(L2 cache 可能也有相似设计)。bank 数量个别与 warp 大小或者 CUDA core 数量对应,例如 32 个 core 就把 SMEM 划分为 32 个 bank,每个 bank 蕴含多个 cache line。Bank 能够了解为 Memory 的对外窗口,有 10 个窗口能够拜访,必定要比只有 1 个窗口要高效。
如果同一个 warp 中的不同线程,拜访的是不同的 bank,那么就能够并行执行,最大化利用带宽,性能最高。
如果拜访的是一个 bank 中的同一个 cache line,那么能够通过播送机制同步到其余线程。一次拜访即可获得数据,也不会有性能问题。
如果拜访的是同一个 bank 中的不同的 cache line,那么就必须阻塞期待,串行拜访。这个会重大妨碍 GPU 的并行性,产生显著的性能降落。这个阻塞期待的状况,被称为 Bank Conflict。
如果不同的线程,对同一个 cacheline 有写操作,那么也必须要阻塞期待。必须等上一个线程写结束,能力执行前面的读取或者写入操作。
七、GPU 的运算零碎
-
SIMD (Single Instruction Multiple Data) 和 SIMT (Single Instruction Multiple Thread)
在游戏引擎内,咱们常会应用 SSE 减速计算(比方视锥体裁剪的计算)。这里利用的就是 SIMD,单个指令计算多个数据。
而 GPU 的设计是为了满足大规模并行的计算(其解决的工作就是人造并行的)。因而 GPU 是典型的 SIMD/SIMT 执行模式。在其外部,若干雷同运算的输出会被打包成一组并行执行。
在介绍 SIMT 之前,咱们须要先介绍下 Vector processor 和 Scalar processor 的概念。晚期 GPU 是 Vector processor(对应 SIMD)。因为晚期 GPU 解决的都是色彩值,就是 rgba 四个重量。在此架构下,编译器会尽可能把数据打包成 vec4 来进行计算。然而随着图形渲染以及 GPGPU 的倒退,计算变得越来越简单,数据并不一定可能打包成 vec4,这就可能会导致性能的节约。所以古代 GPU 都改良为 Scalar processor(对应 SIMT)。前面介绍 Mali 的架构演进的时候还会提到这一点。
古代 GPU 都是 SIMT 的执行架构。传统 SIMD 是一个线程调用向量处理单元 (Vector ALU) 操作向量寄存器实现运算,而 SIMT 往往由一组标量处理单元 (Scalar ALU) 形成,每个处理单元对应一个像素线程。所有 ALU 共享管制单元,比方取指令 / 译码模块。它们接管同一指令共同完成运算,每个线程,能够有本人的寄存器,独立的内存拜访寻址以及执行分支。
传统的 SIMD 是数据级并行,DLP (Data Level Parallelism)。而 SIMT 是线程级并行,TLP (Thread Level Parallelism)。更进一步的超标量(Super Scalar)是指令级并行,**ILP (Instruction Level Parallelism)**。
Mali 的 Midgard 是 VLIM(超长指令字,Very long instruction word)设计。它能够通过 128bit-wide 的计算单元并行计算 4 个 FP32 或者 8 个 FP16 等类型的数据。编译器和 GPU 会合并指令以充分利用处理器资源。这也是一种指令级并行(ILP)。
PowerVR、Adreno 的 GPU,以及 Mali 最新的 Valhall 架构的 GPU 都反对 Super Scalar。能够同时发射多个指令,由闲暇的 ALU 执行。也就是说,同一个 Pipeline 内的多个 ALU 元件是能够并行执行指令序列中的指令的。
无论应用哪种架构,GPU 的计算单元都是并行处理大量数据,所以有的文章也会间接把 GPU 的计算单元称作 SIMD 引擎,或者简称为 SIMD。
如上图所示,左侧是 Vector 处理器,而右侧是 Scalar 处理器。对于 Vector 处理器而言,它是在一个 cycle 内计算 (x,y,z) 三个值,如果没有填满的话,就会产生节约。如果是 Scalar 处理器,它是在 3 个时钟周期内别离计算 (x,y,z) 三个值,不过它能够 4 个线程同时计算,这样就不会节约处理器性能。
合并单个计算为向量计算,在 Scalar processor 上没有优化成果。因为处理器计算的时候还是会把向量拆散。之前是一个 vec4 在一个 cycle 内实现计算。当初是一个 vec4 在 4 个 cycle 内实现计算,每个 cycle 计算一个单位。如果是 vec3 的话,就是 3 个 cycle。
-
Warp 线程束
Warp 是典型的单指令多线程(SIMT)的实现。32 个线程为一组线程束(Warp)。这 32 个线程同时执行一样的指令,只是线程数据不一样,这就是锁步(lock step)执行。这样的益处就是一个 Warp 只须要一个套管制单元对指令进行解码和执行就能够了,芯片能够做的更小更快。Mali 晚期的 GPU 并不是基于 warp 的,性能体现不佳。因为每个外围都有管制单元,占用了过多的晶体管,产生了很多的 overhead,进而导致功耗的减少。
Warp Scheduler 会将数据存入寄存器,而后将着色器代码存入指令缓存,并要求指令分派单元(DispatchUnit)从指令缓存中读取指令分派给计算外围(ALU)执行。
Warp 中的所有线程执行的是雷同的指令,如果遇到分支,那么就可能会呈现线程不激活执行的状况(例如以后的指令是 true 的分支,然而以后线程数据的条件是 false),此时线程会被遮掩(masked out),其执行后果会被抛弃掉。shader 中的分支会显著减少工夫耗费,在一个 warp 中的分支除非 32 个线程都走到 if 或者 else 外面,否则相当于所有的分支都走了一遍。
线程不能独立执行指令而是以 warp 为单位,而这些 warp 之间才是独立的。warp 是 GPU 执行的最小单位。如果一个 shader 对应的像素数量填不满 32 个线程,它也会占用一个 warp 来执行。这种是显著 warp 利用率低的状况,局部外围在工作,局部外围在陪跑。
一个 Warp 中的像素线程能够来自不同的图元。只不过其 shader 指令是统一的。
Warp 中的线程数量和 SM 中的 CUDA core 数量并不一定是统一的。齐全能够 Warp 为 32,然而 CUDA core 只有 16 个。这种状况下,每个 core,两个 cycle 实现一个 warp 的计算就行了。PowerVR 就是相似的设计。
-
Stall 和 Latency Hiding (提早暗藏)
指令从开始到完结耗费的 clock cycle 称为指令的 latency。提早通常是由对主存的拜访产生的。比方纹理采样、读取顶点数据、读取 Varying 等。像纹理采样,如果 cache missing 的状况下可能须要耗费几百个时钟周期。
CPU 通过分支预测、乱序执行、大容量缓存等技术来暗藏提早。而 GPU 则是通过 warp 切换来暗藏提早。
对 CPU 而言,上线文切换是一个有显著开销的行为。所以 CPU 是尽可能防止频繁的线程切换的。而 GPU 在 Warp 之间切换简直是无开销的,所以当一个 Warp stall 了,GPU 就切换到下一个 Warp。等之前的 Warp 取得须要的数据了,再切换回来继续执行。
之所以可能实现这个机制,得益于 GPU 的大寄存器设计。GPU 中的寄存器数量远超 CPU。比方前文提到的费米架构的 GPU,每个 SM 的寄存器是 3 万个。而每个线程可能应用的最大寄存器数量限度在 255。所以即使每个线程都占满寄存器,也只耗费了总寄存器数量的四分之一。
每个 SM 会被同时调配多个 warp 来执行,warp 一旦进入 SM 内就不会再来到,直到执行结束。每个线程会在一开始就调配好所需的寄存器和 Local Memory。当一个 Warp 产生了 Stall,GPU 的 Core 会间接切换到另外的 warp 来执行。因为不须要保留和复原寄存器状态,所以这个切换简直没有老本,能够在一个 cycle 内实现。
SM 中 warp 调度器每个 cycle 会筛选 Active warp 送去执行,一个被选中的 warp 称为 Selected warp。没被选中,然而曾经做好筹备被执行的称为 Eligible warp,没筹备好要执行的称为 Stalled warp。warp 适宜执行须要满足两个条件:32 个 CUDA core 有空以及所有以后指令的参数都准备就绪。
如果 Shader 外面应用的变量越多(Shader 写的很长),占用的寄存器数量就越多,留给 Warp 切换的寄存器就会变少。调配给 SM 的 Warp 数量就缩小了,也就是 Active warp 升高了。这会升高 GPU 暗藏提早的能力,会升高 GPU 的利用率。
对于 GPU 的执行过程,知乎上洛城的这篇答复十分乏味,活泼形象的展现了 GPU 的硬件形成和常见概念。如果对其还不理解的同学,强烈推荐浏览。
- Warp Divergence
因为 Warp 是锁步执行的,Warp 中的 32 个线程执行的是同样的指令。当咱们的 shader 中有 if-else 的时候,如果 Warp 内有的线程须要走 if 分支,有的线程须要走 else 分支,就会呈现 Warp divergence。GPU 对此的解决形式是,两个分支都走一遍,通过 Mask 遮蔽掉不要的后果。
如果 Warp 内所有线程都走的是分支的一侧,则没有太大影响。所以动静分支就相当于两条分支都走一遍,对性能影响较大,而动态分支则还好。当然,理论状况可能还会更加简单一些,前面会再具体探讨。
八、其余重要概念
- Pixel quad
光栅化阶段,栅格离散化的粒度尽管最终是像素级,然而离散化模块输入的单位却不是单个像素,而是 Pixel Quad(2×2 像素)。其中起因可能是单个像素无奈计算 ddx、ddy,从而在 PS 当中判断选用贴图的 mipmap 层级会产生艰难。进行 EarlyZ 断定的最小单位也是 Pixel Quad。
如上图所示,能够看出像素点网格被划分成了 2X2 的组,这样的组就是 Quad。一个三角形,即便只笼罩了一个 Quad 中的一个像素,整个 Quad 中的四个像素都须要执行像素着色器。Quad 中未被笼罩的像素被称为 ” 辅助像素 ”。比拟小的三角形在渲染时,辅助像素的比例会更高,从而造成性能节约。
请留神,辅助像素其实仍然在管线内参加整个 PS 计算,只不过计算结果被抛弃而已。而又因为 GPU 和内存之间有 cache line 的存在,cache line 一次替换的数据大小是固定的,所以这些被抛弃的像素很多时候也不会节俭带宽。他们会原样读入原样写出,带宽耗费还是那么多。所以,尽可能防止大量小图元的绘制,能够更无效的利用 Warp。
- EarlyZS
Depth test 和 Stencil test 是一个硬件单元(ROP 中的硬件单元)。Early depth test 的阶段同样是能够做 Early stencil test 的。所以很多文档会形容这个阶段为 Early ZS。Early-Z 技术能够将很多有效的像素提前剔除,防止它们进入耗时重大的像素着色器。Early-Z 剔除的最小单位不是 1 像素,而是像素块(Pixel Quad)。
传统的渲染管线中,depth test 在像素着色器之后进行。进行深度测试,发现自己被遮挡了,而后抛弃掉。这显然会呈现大量的无用计算,因为 overdraw 是不可避免的。因而古代 GPU 中使用了 Early-Z 的技术,在像素着色器执行之前,先进行一次深度测试,如果深度测试失败,就不用进行像素着色器的计算了,因而在性能上会有很大的晋升。
AlphaTest 会影响 EarlyZ 的执行。一方面是本身不能执行 EarlyZ write 操作,因为只有当像素着色器执行结束之后才晓得本人要不要抛弃,如果提前写入深度会有谬误的执行后果。另外一方面只有当本人执行完像素着色器,写入深度之后,雷同地位的后续片元能力继续执行,否则就必须阻塞期待其返回后果,这会阻塞管线。对于这一点前面还会再做详细分析。
其余如在像素着色器外面批改深度,或者应用 Alpha to coverage 等,也会影响 EarlyZ 的执行。
- Hierarchical-z 和 Tile-based Rasteration
这两个是硬件提供的优化。
Hierarchical Z-culling,也称为 Z-cull。是 NVIDIA 硬件反对的粗粒度的裁剪计划。有点像 Adreno 的 LRZ 技术,通过低分辨率的 Z-buffer 来做剔除。不过它只准确到 8×8 的像素块,而非像 LRZ 一样能够准确到 Quad(2×2)。挪动平台 GPU 有其余技术做裁剪剔除,所以猜测是没有应用这个技术的。另外,不要把它和 EarlyZ 弄混,也不要把它和咱们引擎实现的 Hi-Z GPU Occlusion Culling 弄混。
Tile-based Rasteration 技术。光栅化也是能够 Tile-based,这同样是硬件厂商的优化技术。光栅化阶段通常不会成为性能瓶颈。不过游戏性能优化杂谈中介绍了一个乏味的案例,原神中对树叶应用 Stencil,冀望通过抠图实现半透明成果来进步性能。然而却因为影响了 Tile-based Rasteration 的优化,反而导致性能降落。在 PC 平台原本会有一些优化习惯,比方通过 discard 或者其余伎俩剔除掉像素,防止其进入到像素着色器(缩小计算)或者 ROP(缩小拜访主存)阶段,以此来进步性能。不过这些习惯在挪动平台通常都是负优化。
大量小三角形绘制是 GPU 十分不善于的工作情景。GPU 对顶点着色器和光栅化的优化伎俩无限,又因为光栅化的输入是 PixelQuad,那么大量像素级的小三角形就必然会导致 warp 中的无效像素大大减少。所以 UE5 的 Nanite 会应用 ComputeShader 本人实现软光栅,来代替硬件光栅化解决这些像素级的小三角形,以此取得几倍的性能晋升。相干细节能够参考 UE5 渲染技术简介这篇文章。
- Register Spilling 和 Active Warp
GPU 外围的寄存器尽管很多,然而数量还是无限的。GPU 外围执行一个 Warp 的时候,会在一开始就把寄存器调配给每条线程。如果 Shader 占用的寄存器过多,那么可能调配到 GPU 外围来执行的 Warp 就更少。也就是 Active Warp 升高。这会升高 GPU 暗藏提早的能力,进而影响 GPU 的性能。比方,本来在一个 Warp 加载纹理产生 Stall 的时候,会切换到下一个 Warp,如果 Active Warp 过少,就可能所有 Warp 都在期待纹理加载,那么此时 GPU 外围就真的产生 Stall 了,只能空置期待后果返回。
寄存器文件会用多少,在 shader 编译完就确定了。每个变量、长期变量、局部符合条件的 uniform 变量,都会占用寄存器文件。如果 Shader 应用的寄存器文件过多,比方超过 64 或者 128,会产生更加重大的性能问题,就是 Register Spilling。GPU 会将寄存器文件存储到 Local Memory 上,之前咱们介绍过,LocalMemory 就是主存的一块儿区域,访问速度是很慢的,所以 Register Spilling 会大大降低 Shader 的执行性能。
Shader 占用的寄存器文件多少,指令数多少,是否产生 Spilling,都能够应用 Mali offline compiler 查看。
- Mipmap
咱们传递给 GPU 一个带 Mipmap 的纹理,GPU 会在运行时通过 (ddx, ddy) 偏导选取适合的 Mipmap Level 的纹理。
Mipmap 有利于节俭带宽,并不是说咱们传递给 GPU 的纹理数据变小的(相同是减少了)。而是最终渲染的时候相邻的像素更有可能在一个 CacheLine 外面,这就进步了 Texture cache 的命中率。因为缩小了对主存的交互,所以缩小带宽。
后面咱们介绍 GPU 内存的时候有提到,当须要拜访主存的时候,须要耗费几百个时钟周期。这会产生重大的 Stall。晋升 Texture Cache 命中率就能够缩小这种状况的呈现。咱们通过一些 GPU 性能剖析工具优化游戏性能的时候,Texture L1/L2 Cache Missing 是一个十分重要的指标,通常要管制在一个很低的数值才是正当的。
Mipmap 自身是会多耗费 1/3 的内存的(多了低级别的 mipmap 图),不过咱们是能够决定纹理 Upload 给 GPU 的最高 mipmap level。咱们通过引擎动态控制纹理的最高 mipmap level,反而能够无效的管制纹理的内存用量,这就是 Unity 引擎的 Texture Streaming 机制。基于 Texture Streaming,纹理的内存总量是固定的,把不重要的纹理换出成高 level 的 mipmap 就能够缩小纹理的内存占用。当然如果从新切换到 mipmap0,可能会有纹理加载的过程,不过这个是引擎外部实现的,下层开发者是无感知的。咱们看到很多 3D 游戏图片会有从含糊到清晰的过程,有可能就是 Texture Streaming 在起作用。
对于纹理的内存占用这里能够再做补充阐明。后面介绍挪动平台 GPU 内存的时候咱们有提到,尽管 CPU 和 GPU 是共用一块儿物理内存,然而其内存空间是拆散的。所以纹理提交给 GPU 是须要 Upload 的。当纹理 Upload 给 GPU 之后,CPU 端的纹理内存就会被开释掉。这种状况下,咱们将显存中的纹理的内存开释掉,也就相当于开释掉纹理内存。
在 Unity 中,还有一部分纹理是须要 CPU 端读写数据的,或者编辑器下某个纹理导入选项中勾选了 Read/Write Enabled。对这些纹理而言,CPU 端的内存就不会被开释掉。此时该纹理就占用了两份内存,CPU 一份,GPU 一份。
- 纹理采样和纹理过滤
纹理过滤有几种模式:
1. 邻近点采样 Nearest Point Sampling
2. 双线性插值 Bilinear Interpolation
3. 三线性插值 Trilinear Interpolation
4. 各向异性过滤 Anisotropic Filter
古代 GPU 都反对一个 cycle 内实现一个 Bilinear 的采样。从性能上说,Point Sampling 和 Bilinear Filtering 是一样的。较新的高端 GPU,如 Mali-G78 能够在 0.25 个 cycle 实现一个 Bilinear 的采样。也就是说它能够在一个 cycle 内实现一个 Quad 的采样。
Trilinear Filtering 须要采样两层 mipmap 做插值,所以耗费是 Bilinear 的两倍,也就是两个 cycle 一个采样。
N 倍各向异性就是 N 倍开销。
九、从硬件角度了解 GPU 的执行逻辑
-
GPU 中的可编程元件和固定管线元件
1. 顶点和像素解决是可编程,在 Shader Core 中执行着色器指令。
2. 光栅化是不可编程的,由光栅化引擎负责。
3.EarlyZ、LateZ、Blend,是固定管线,由 ROP 单元负责。
4. 固定管线的单元负责特定工作,硬件制作更加简略,性能更好,功耗更低。
-
从硬件角度看 EarlyZ
咱们在游戏引擎级别看,每个 drawcall 有它对应的 RenderState,以此来决定是否是 AlphaBlend、是否要写深度、是否是 AlphaTest 等等。然而对于硬件而言,每个图元并不知道本人是不是 AlphaBlend。以后 RenderState 是 AlphaBlend 的话,那么图元就依照 AlphaBlend 绘制。以后的 ZWrite 是 Off 的,那么 LateZ 就不写深度。
执行 EarlyZ 的是硬件单元(ROP),所以不应该用代码的思维去了解 EarlyZ 的执行过程,更失当的比喻应该是流水线上的阀门,它能够管制片元是否通过。有一些咱们用软件实现起来不言而喻的算法,在硬件上却是十分低廉,难以实现的计划。
-
GPU 外围的乱序执行和保序
GPU 的计算外围是乱序执行的,不同 Warp 执行耗时不统一。受分支、cache missing 等因素影响。GPU 会尽可能填充工作到计算外围。
然而同一个像素的写入程序是能够失去保障的。先执行的 drawcall 的像素肯定是先写入到 Framebuffer 中的。不同像素的写入程序通常也是有序的。
GPU 在每个阶段的输入后果其实都是有序的。不同阶段之间,通过 FIFO 队列,保障程序。随着技术的倒退,可能应用的技术不限于 FIFO,然而最终目标都是保序。
这个机制是有现实意义的。对于半透明物体,如果 ROP 是乱序的,那么失去的是谬误的后果。而对于不通明物体,尽管有 Depth Test 的机制,乱序也能够保障后果正确,然而保序对性能有益处,且能够缓解 Z-fighting。
挪动平台 GPU 架构
一、PowerVR 架构
-
PowerVR GPU 管线
A10 之前(iPhone7),都是 Imagination PowerVR 的 GPU,GPU 架构能够参考 Imagination 的文档。A11 (iPhone8/iPhoneXR)开始应用的是苹果自研 GPU,苹果应该是失去了 Imagination 的受权,所以 HSR 等个性仍然是保留的。苹果自研的 GPU 相干材料较少,临时了解为是 PowerVR 的加强衍生版本。
TBDR 的第一步 Tiling 的后果寄存在 Parameter Buffer 中。Parameter Buffer 是 System memory 上的一块儿数据区。它的大小是无限的。所以可能会呈现 PB 曾经被填充斥,然而还有 drawcall 未执行的状况。当这种状况呈现的时候,硬件会进行 Flush,以便后续的渲染可能继续执行上来。这带来的问题是 Flush 前和 Flush 后的对象是两次 HSR 解决,即使存在遮挡也无奈正当的进行剔除,导致 overdraw 减少。当 PB 填充斥的时候,可能会导致性能急剧下降。所以应该简化场景,避免出现这种状况。
GPU 通过 ISP 单元进行 HSR(Hidden Surface Removal,隐面剔除)解决。ISP 同样会解决深度回读(Visibility feedback)的状况。HSR 是 EarlyZ 的齐全替代品。能够像素级的剔除被遮挡的片元。HSR 处理结果寄存在 TagBuffer 中,TagBuffer 在片上缓存里,通过 TagBuffer 就能够失去最终须要绘制的片元。只有最终对屏幕产生奉献的像素才会被绘制。
对于 AlphaTest 对 HSR 的影响能够参考前面专门对 AlphaTest 的探讨简略说就是 AlphaTest 不会影响本身的剔除断定,然而会卡管线。它会打断 ISP 解决笼罩同一像素的几何体。ISP 要失去 PS 执行后的后果能力正确进行 HSR,在这一过程中,所有笼罩了带有 discard 操作像素的几何体全副都要期待。
-
PowerVR GPU 硬件架构
PowerVR Rouge 架构的 GPU 蕴含了 N 个 Unified Shading Cluster,这个 USC 就是 GPU 的外围。每个 USC 蕴含 16 条 Pipeline。每个 Pipeline 蕴含 N 个 ALU。ALU 就是真正执行指令的中央。ALU 的数目是 GPU 性能的重要指标。
从上图咱们能够看到 PowerVR 是两个 USC 共享一个 Texture Unit。
图中的 MCU 就是 L2 缓存。每个 USC 和 TU 都装备独立的 L1 缓存。每个 USC 中还都有一块儿 Tile Memory 也就是咱们之前说的 On-chip memory。
Rouge 架构中,每个 Pipeline 蕴含 4 个 FP16 的 ALU,2 个 FP32 的 ALU 和 1 个 SFU。能够看到 FP16 和 FP32 的 ALU 是拆散的。尽管这会占用更多的芯片面积,然而能够大幅缩小功耗。FP16 速度更快,占用带宽更小,功耗更小。
PowerVR 是 Scalar ALU。反对超标量(Super Scalar),能够同时发射多条指令,让闲暇的 ALU 来执行。所以一个 Pipeline 外部的多个 ALU 是能够被充分利用起来的。
每个 Pipeline 内多个 ALU 的设计跟其余 GPU 也有些不一样。比方 NVIDIA 的 CUDA Core 就只有两个 ALU,一个 FP32 ALU 和一个 INT ALU。所以尽管 PowerVR GPU 的外围(USC)数量尽管不多,然而 ALU 数量反而会比同档次 GPU 要多。
PowerVR 的 Warp 是 32 大小。然而它的 Pipeline 是 16。所以它是两个 cycle 解决一个 Warp。
二、Mali 架构
-
Mali GPU 管线
Mali GPU 中有两条并行的管线,Non-Fragment(解决 Vertex Shader、Compute Shader)和 Fragment(解决 Fragment Shader)。
下面的图中能够确认,FPK(Mali 的隐面剔除)是在 EarlyZ 之后的。
Execution Core 中,有 WarpManager 负责 Warp 的调度。指令执行单元有 FMA(fused multiply-accumulate,混合乘加,根底浮点计算)、CVT(convert,类型转换)、SFU(special functions unit,非凡函数计算)三个元件。在 Valhall 架构下反对 SuperScalar,这三个元件是能够并行执行指令的。
-
Mali GPU 四代架构演变
Mali GPU 的架构演变十分直观的展现了挪动 GPU 的进化过程。再加上 Mali 的开发材料比拟多,所以这里别离介绍了 Mali 的四代架构。这里能够和上文介绍的 GPU 管线和硬件架构的实践造成参考和对照。
Utgard (2007)
这是 Mali 的第一代架构。对应 Mali-4xx 系列的 GPU。
这代 GPU 并非 Unified shader core,Vertex 和 Pixel 应用不同的计算单元。在市面上简直看不到了。
Midgard (2012)
Midgard 是 Mali 的第二代 GPU 架构,见于 Mali-T8xx, Mali-T7xx 和 Mali-T6xx。市面上并不多见了,可能在智能电视芯片中还可见到。
Midgard 的 Shader 外围曾经是 Unified shader core。指令执行单元叫做 Tripipe,外部蕴含三个单元:
1.ALU(s) — Arithmetic Pipeline,执行指令的中央。可能有 2~3 个。
2.Texture Unit,配有 L1 缓存。
3.Load/Store Unit,配有 L1 缓存。
Midgard 是 Vector processor,通过 SIMD 实现并行计算的。此时并没有 Warp 机制。应用 128-bit wide 的 ALU 进行计算。能够混合解决不同类型的数据,比方 4 个 FP32,8 个 FP16 或者 16 个 INT8。
网上可能会见到的在 shader 中做 vector 解决合并数据来进步性能,对应的就是 Madgard 这种 Vector 处理器。这种优化措施对前面的 Scalar 处理器曾经不再实用。
Midgard 的 Shader Core 是以指令级并行(ILP,Instruction Level Parallelism)为主的设计,采纳的是超长指令字(VLIW)指令格局。为了最大水平地利用 Midgard 的 Shader Core,须要提取尽可能多的指令(4 条 FP32 并发指令),以便填充 Shader Core 中的所有槽。这种设计非常适合根本的图形渲染工作,因为 4 种颜色重量 RGBA 非常适合 VLIW-4 设计的 4 条通道。
随着挪动 GPU 技术的倒退,解决方案逐步向标量解决转移,即以线程级并行(TLP,Thread Level Parallelism)为核心的体系结构设计。这也正是其下一代 Bifrost 架构的倒退方向。指令向量化不肯定可能完满执行,可能有的标量无奈向量化,导致时钟周期的节约。新的设计不会从单个线程中提取 4 条指令,而是将 4 个线程组合在一起并从每个线程中执行一条指令。以 TLP 为核心设计的劣势在于它无需破费大量精力即可从线程中提取指令级并行性。同时对编译器也更加敌对,编译器能够实现的更加简略。
Bifrost (2016)
Bifrost 是 Mali 的第三代架构 GPU,见于 Mali-G71、G72、G76 和 Mali-G5x。
Mali 的着色器外围数量是可变的。从下面的 die shot 能够看到,Mali-G76MP10 蕴含 10 个 Shader core,MP 代表了外围数量。不同外围数量性能差别十分大。所以同样是 Mali-G72 架构,Mali-G72MP12 能跑规范画质,而 Mali-G72MP3 就只能跑晦涩画质了,其性能甚至还不如 Mali-G71MP8。
Bifrost 每 Shader core 蕴含 3 个 Execution Engine(指令执行单元),中低端的 Mali-G5x 可能每个 Shader core 只蕴含 2 个 EE。
Bifrost 的执行外围不再是 Tripipe 构造。Bifrost 把 TextureUnit 和 L/S Unit 从 Execute Engine 中拆离开了。变成 Shader core 中的独立单元。这样能够防止负载不平衡导致 TU 的能力被节约,同时也更容易扩大 ALU,加强 GPU 的计算能力。
从这一代开始,Mali GPU 从 Vector 处理器转变为 Scalar 处理器。对应的也退出了 Warp 机制。一个 Warp 是 4 个线程,Mali 称其为 Quad。相比于 NVIDIA 或者 PowerVR 的 32 线程 Warp,Bifrost 的 Warp 要小很多。Warp 小,那么呈现上文介绍的 Warp Divergence 的时候就能够避免浪费,也就是说 if-else 分支对其影响较小。不过 Warp 小,意味着须要更多的管制单元,比方 32 线程的 Warp 只须要 1 个管制单元,而到 Mali 这边就须要 8 个管制单元。管制单元过多,会占用更多的晶体管和芯片面积,限度了 ALU 的数量。同时也意味着更多的功耗。
Mali-G76 在此架构根底上减少了一条通路,每个 EE 能够同时解决 8 条线程,也就是说 Warp 大小扩大为 8 了。架构没有扭转,把 Warp 进步一倍,就带来了大幅的性能晋升。可见之前 4-wide warp 的设计并不是正确的抉择。
Mali GPU 的 ALU 是不辨别 FP32 和 FP16 的。像 Midgard 一样,GPU 会做指令的合成和交融。其 ALU 每个时钟周期能够解决 1 个 INT32、2 个 INT16 或 4 个 INT8。像 PowerVR,FP32 和 FP16 ALU 是独立的,这也是在空间和功率效率之间进行衡量的抉择。独立 ALU 会占用更多的芯片面积,然而会缩小功耗。从后果上看,PowerVR 的设计更加正当,前面会介绍的 Mali-G78 重写的 FMA(Fused-Multiply-Add)也批改为 FP32 和 FP16 独立 ALU 的设计了。
Valhall (2019)
这是 Mali 最新的 GPU 架构,Mali-G77、G78 以及最新推出的 G710 都是这个架构。对应的中低端架构为 G5XX。
从这一代开始,不再是每个 Shader core 三个 Execution Engine 了。而是一个 Execution Engine。不过 EE 改良为两个 16-wide 的构造。也就是说从 Mali-G77 开始 Warp 大小批改为 16 了。同时这代开始的 GPU,都是 Super Scalar 设计,能够更好的利用 ALU 闲暇单元,晋升流水线性能。
后面有提到,G78 这一代,重写了 FMA 引擎,其 ALU 也变为 FP32 和 FP16 独立元件了。
当初掂量 GPU 性能的一个重要指标是 Floating-point Operations 的能力。联合 GPU 外围的时钟频率就能够失去 FLOPS(Floating-point Operations Per Second),也就是咱们在跑分软件外面看到的 GFLOPS。须要留神的是,FMA(Fused-Multiply-Add,a x b + c)或者叫 MAD,也就是乘加,一次执行记做两个 FLOP。
上面列举不同架构,单核心每个时钟周期的 FP32 operations 数量(FP32 operations/clock)。
1.Mali-G72 是 3 x 4 x 2 = 24
2.Mali-G76 是 3 x 8 x 2 = 48
3.Mali-G77 是 16 x 2 x 2 = 64
4.Mali-G710 是 16 x 2 x 2 x 2 = 128
能够看到,Mali-G77 尽管只有一个 EE,然而计算能力相比 G72 和 G76 却大幅晋升。同时因为管制单元就更少,其管制单元的 overhead 就更少。执行雷同的运算的功耗就更低。最新的 Mali-G710,架构不变,EE 扩大为两个,性能再次大幅晋升。
上述数据能够在这里[Arm Mali GPU datasheet](https://developer.arm.com/-/m… Community/PDF/Mali GPU datasheet/Arm Mali GPU Datasheet 2021.2.pdf?revision=82e2cd30-98cd-4a10-bbe9-70ab4ce1e7d3&hash=35BF874DD8C0F48BCBE09AA50719CDEB933DA4BE) 下载到。这个文档蕴含 Mali 每个 GPU 的具体数据,比方 Warp 大小,L1 cache 大小等等。
-
Mali GPU 其余技术
Forward Pixel Kill
FPK 是 Mali 的隐面剔除技术。通过 EarlyZ 的片元会进入到一个 FIFO 的 FPK 队列。如果前面的片元发现后面的片元被遮挡住了,那么就能够将其终止掉。失常状况下一个片元通过 EarlyZ 断定之后,就产生了 pixel thread,会提交给 Shader core 来执行,这个 thread 是无奈终止的。不过 Mali 的 FPK 技术却能够终止掉不须要绘制的 thread,从而防止 overdraw。
相比 PowerVR 的 HSR,FPK 更像是一个 EarlyZ 的硬件补丁,来补救 EarlyZ 的有余。当 EarlyZ 生效的时候,FPK 必定也是生效的。FPK 并不能保障被遮挡的不通明像素肯定不会被绘制(比方队列中的 Fragment 曾经被解决了)。所以 Mali 举荐还是应用排序的形式以充分利用 EarlyZ 进行剔除。这篇官网文档的优化倡议中也提到了排序的问题。尽管有 FPK,然而不要过分依赖它,该排序还是要排序的。
IDVS: Index-Driven Vertex Shading
Vertex shading 被拆成两个局部,Position Shading 和 Varying Shading。计算完 position shading 就能够进行裁剪,只有通过裁剪的图元才会执行 varying shading。这样就被裁掉的图元就不必 fetch 各种属性甚至纹理了。
所以对于 Mali GPU 而言,把 Mesh 的 position 独自拆分一个 stream 能够无效节俭带宽。其余 GPU 应该也有相似的技术。而对高通的 Adreno 而言,因为 LRZ 须要先跑一遍 VertexShader 中的 Position 局部,失去低分辨率深度图,所以对其而言拆分 position 能够取得更大的收益。
AFBC: Arm Frame Buffer Compression
AFBC 是 FrameBuffer 的疾速无损压缩。能够节俭带宽,也能够升高显存占用。这个对开发者是无感知的。其余平台也都有相似的压缩技术。
Transaction Elimination
Transaction Elimination 也是一种很无效的升高带宽的办法。在有些状况下,只有局部 Tile 中的内容会变动(例如摄像机不动,一个 Tile 中只有动态物体)。此时通过比拟该 Tile 前一次和本次的渲染后果的 CRC 值,可失去 Tile 是否变动,如果不变,那么就没有必要执行 Tile 到 System Memory 的写回操作了,无效地升高了带宽占用。
Hierarchical Tiling
依据图元的大小抉择适合的 Hierarchy Level 的 Tile。升高 Tiling 阶段对主存的读取和写入开销。
Shared Memory
Mali GPU 没有 Shared Memory。所有对 Shared Memory 的操作其实都是寄存器或者主存。不过这个影响比拟大的是 OpenCL 计算或者 ComputeShader。一般的 shader 也操作不了 Shared Memory。
Adreno GPU 有 Shared Memory。
三、Adreno 架构
Adreno3xx, 4xx, 5xx, 6xx 是市面上常见的型号。都是 Scalar 架构。
1.3xx 在一些非常低配的手机上还能够见到。
2.5xx 个别见于中低配手机。这一代开始退出了 LRZ 技术。
3.6xx 是近几年新出的型号。630~660 是高配,如骁龙 888 装备是 Adreno660 芯片。
Adreno 一个十分显著的特点是它的外围数量很少,然而每个外围装备一个十分大的 GMEM,这个 GMEM 是 On-chip 的,大小能够达到 256k~1M。比方只有 Adreno630 只有 2 核,GMEM 大小为 1MB。
Adreno 上的 Bin(也就是 Tile)并不是固定大小。而是依据 GMEM 大小和 RenderTarget 格局决定。其大小通常比 Mali 的 16×16 要大十分多。因而,如果渲染指标如果开启 HDR+MSAA 的话,Bin size 会小很多,也就意味着更多的与主存的交互,明显增加功耗。
Flexable Render
Adreno GPU 同时反对 IMR 和 TBR 两种模式,并且能够依据画面的复杂度,在两者之间动静切换,这就是 Flexable Render 技术。当然,在挪动平台 TBR 仍然是更为高效的形式。
Low Resolution Z
LRZ 是 Adreno5xx 系列开始退出的隐面剔除技术。通过先跑一遍 Vertex Shader 的 position 局部,生成低精度的深度图,进行裁剪剔除。相当于硬件级别做了 Hi-z。这个剔除是能够准确到 Pixel Quad 的。
四、总结
1.Mali 的 Warp 是 16,PowerVR 是 32。Adreno 没有找到相干材料,大概率是 32。
2.PowerVR、Adreno 和 Mali 最新的 Valhall 都是 Scalar 架构,反对 SuperScalar,能够更好的利用 ALU 并行计算。
3. 隐面剔除技术,PowerVR 是 HSR,Adreno 是 LRZ,Mali 是 FPK。
4. 把 Mesh 的 position 独自拆分一个 stream,有利于节俭带宽。
5.Shader 中应用 mediump,半精度浮点数,能够更好的利用 FP16 的 ALU,性能更好。
6.Tile 大小跟 RenderTarget 格局无关。Adreno 的 GMEM 很大,Tile 也要比 Mali 和 PowerVR 要大很多。
常见问题的剖析与探讨
一、DrawCall 对性能的影响
GPU 工作在内核空间(Kernel Space),咱们只能通过驱动与其打交道。所以咱们应用层设置一个渲染命令或者给 GPU 传输数据,须要通过图形 API 和驱动的直达,能力最终达到 GPU。而且驱动调用会有用户空间(User Space)到内核空间的转换。当 DrawCall 十分大的时候,这里的 overhead 就会很高。
以 DX 为例,程序提交一个 DrawCall,数据须要通过 App->DX runtime->User mode driver->dxgkrnl->Kernel mode driver->GPU,而后能力达到 GPU。在达到 GPU 之前,全副是在 CPU 上执行的。这也是新的 DX12 试图升高的开销。
主机平台的硬件是固定的,能够对其硬件和驱动做专门的优化。没有了驱动的层层直达,CPU 和 GPU 交互的开销是很低的。所以即使主机的硬件性能不如 PC,然而理论游戏性能却远超 PC。
当然,单纯的 DrawCall 命令(比方 DrawPrimitive)开销也不会很大。更大的开销在于 DrawCall 附带的绑定数据(buffer、texture、shader),设置渲染状态的开销。在 RenderDoc 能够看到一个 DrawCall 实际上可能会有十几条命令。
在 Unity 中,绑定 Vertex buffer 记做一个 Batch。CPU 和 GPU 的交互模式,更加善于一次传输大量数据,而不是屡次传输大量数据。Unity 的 StaticBatch 和 DynamicBatch 的目标就在于此。当初 Unity 都是以 Batches 数目代指 DrawCall 数目。
材质切换记做一个 SetPassCalls。材质切换会面临大量的属性同步、shader 的编译和绑定、纹理绑定等等,无论在引擎层面还是 GPU 交互层面都是微小开销。古代引擎通过排序,让雷同渲染状态的物体间断绘制,目标就是缩小这一部分的开销。
如果绘制大量小物体,很有可能大量工夫耗费在 CPU 和 GPU 的交互上,而理论 GPU 自身的负载并不高。所以咱们通常认为 DrawCall 过高可能会导致 CPU 呈现瓶颈。
二、AlphaTest 和 AlphaBlend 对性能的影响
事实上,本文呈现的初衷就是解答这个问题。因为要测试 PreZ 对性能的影响(PreZ 就是针对 AlphaTest 和 AlphaBlend 的优化计划),连带着要测试下 AlphaTest 和 AlphaBlend 对不同平台的性能影响。看了网上的一些探讨和文章,后果变得更加困惑了。很多疑难无奈解答,比方 EarlyZ 的实现机制,什么时候 EarlyZ 会生效等等。最终促使我花了很多的工夫精力去学习 GPU 的硬件架构,这才有了这篇文章。
-
桌面平台
桌面平台的 IMR 架构上,AlphaBlend 操作的是 DRAM(读 + 写),如果应用过多,会有显著的 overdraw 和带宽开销。相比起来,AlphaTest 如果 discard 了,PS 中不会有后续计算,也能够防止对 FrameBuffer 的写操作。如果绘制程序正当(从前往后绘制),未被 discard 掉的局部也能够无效遮挡住后续局部,能够加重 overdraw。
所以在桌面平台,很多时候会倡议应用 AlphaTest 代替 AlphaBlend,这可能会带来性能的晋升。
当然如果思考到其对 EarlyZ 或者 Hierarchical Z 的影响(阻塞甚至生效,与具体硬件实现无关)。节俭的带宽开销是否比上述优化带来的价值更大,是须要理论测试能力得出结论的。这可能会因 GPU 不同而产生不同的后果。
-
挪动平台
比拟有参考价值的是上面两个知乎上的探讨
1. 再议挪动平台的 AlphaTest 效率问题
2. 试说 PowerVR 家的 TBDR。文中摘引是 Alpha Test VS Alpha Blend 这里的探讨,算是比拟官网的答复。
Alpha tested primitives will do the following: ISP HSR: Depth and and stencil tests (no writes) Shading: Colours are calculated for fragments that pass the tests Visibility feedback to ISP: After the shader has executed, the GPU knows which fragments were discarded and which where kept. Visibility information is fed back to the ISP so depth and stencil writes can be performed for the fragments that passed the alpha test When discard is used, pixel visibility isn’t known until the corresponding fragment shader executes. Because of this, depth and stencil writes must be deferred until pixel visibility is known. This reduces performance as the pixel visibility information has to be fed back to the ISP unit after shader execution to determine which depth/stencil positions need to be written to. The cost of this can vary, but in the worse case the entire fragment processing pipeline will stall until the deferred depth/stencil writes complete.
以 PowerVR 的 HSR 为例。不通明物体片元是在 HSR 检测通过就写入深度。而 AlphaTest 片元在 ISP 中做 HSR 检测的时候是不能写入深度的。因为只有像素着色器执行结束之后它才晓得本人会不会被抛弃,如果被抛弃则不能写入深度。而如果没有被抛弃,则会将深度信息回写到 ISP 的 on-chip depth buffer 中。在深度回写结束之前,雷同像素地位的后续片元都不能被解决。这就导致阻塞了管线的执行。
EarlyZ 也是相似的问题。可能晚期 EarlyZ 和 LateZ 是共用硬件单元,读和写必须是原子操作。AlphaTest 导致不能进行 EarlyZ write,也就不能进行 EarlyZ test。所以晚期一些文档会形容为 AlphaTest 导致 EarlyZ 生效,直到 Flush。古代 GPU 不存在这个问题。AlphaTest 物体不能做 EarlyZ write,然而仍然能够做 EarlyZ test。当然因为深度回读导致卡管线,是不可避免的。
独自一个 AlphaTest 和 AlphaBlend 比拟,AlphaBlend 可能会比拟快。因为它不存在的深度回读的过程,也不会阻塞后续图元绘制。不过这个影响很有可能只在特定状况下才会比拟显著。而更加常见的状况是多层半透明叠加的状况。此时 AlphaBlend 因为不写深度,齐全无奈做剔除,会导致 overdraw 很高,在挪动平台上很容易呈现性能问题。而 AlphaTest 尽管会因为写深度而阻塞管线,然而也因为会写深度,后续被遮挡的图元(无论是不通明还是半透明)是能够被剔除掉的。所以这种状况下 AlphaTest 可能性能会更好一些。而如果退出 Prez,AlphaTest 性能劣势会更加显著。所以草地渲染应用 PreZ + AlphaTest +(alpha to coverage)是比拟正当的抉择。通常会比应用 AlphaBlend 有更好的性能体现。
不同的测试用例可能会失去不同的测试后果,而个别咱们的测试用例很有可能是利好 AlphaTest,所以会得出 AlphaTest 性能比 AlphaBlend 好的论断。当然,咱们过于深究 AlphaTest 和 AlphaBlend 的性能差别并没有太大意义。因为少数状况下这两者成果不同,不能相互替换。上面做一些总结。
1. 无论是 AlphaTest 还是 AlphaBlend,都不会影响其本身被不通明物体遮挡剔除。RenderPass 中有 AlphaTest 物体,也不会导致前面不通明物体之间的遮挡剔除。
2. 对于比拟小的特效,不要尝试用 AlphaTest 代替 AlphaBlend,这很有可能是负优化,可能会阻塞管线。
3. 对于草地、树叶等交叉遮挡重大的情景,应用 AlphaBlend 性能很低,应该应用 PreZ+AlphaTest。
4.Opaque–>AlphaTest–>Transparent 是正当的渲染程序,打乱这个程序可能会造成显著性能问题。
三、不通明物体是否须要排序
按下面的介绍。Opaque–>AlphaTest–>Transparent 是正当的绘制程序。Opaque 和 AlphaTest 都是不通明物体队列,Transparent 是半透明物体队列。
尤其要留神,AlphaTest 物体不能频繁的和 Opaque 物体交叉绘制(指的是渲染程序上,而不是物体坐标上),否则会重大阻塞渲染管线。半透明物体不能提到不通明物体队列外面,即半透明物体不能交叉到 Opaque 物体绘制,同样会导致重大的性能问题,比方写深度的半透明物体如果在不通明物体之前绘制,会导致 LRZ 整体失效。
对半透明物体而言,因为要进行混合,所以须要从远到近来绘制(画家算法),否则会失去谬误的绘制后果。
对不通明物体而言,在没有隐面剔除性能的芯片上(Adreno3xx),须要保障物体是从近到远进行绘制,能够更好的利用 EarlyZ 优化,也就是说须要进行排序。而有隐面剔除性能的芯片上(PowerVR、Areno5xx、Mali 大部分芯片),不关怀物体的绘制程序,不须要排序,不通明物体不会有 overdraw。
前文介绍 Mali 的 FPK 的时候有提到,FPK 并不能像 HSR 或者 LRZ 一样,对屏幕无奉献的像素必定会被剔除。FPK 可能存在没有即时 kill 掉的状况。所以对于 Mali 芯片,举荐还是在引擎层做排序。Unity 引擎中断定是否须要排序的代码:
bool hasAdrenoHSR = caps->gles.isAdrenoGpu && !isAdreno2 && !isAdreno3 && !isAdreno4;
caps->hasHiddenSurfaceRemovalGPU = caps->gles.isPvrGpu || hasAdrenoHSR;
这里还须要留神,所谓排序,对半透明物体而言,就是依据物体与相机的间隔排序的。这是为了失去正确的渲染后果。当然即使基于物体排序,也还是会有半透明物体渲染程序谬误导致抵触的状况,比方较大物体相互交叉,或者物体本身部件之间相互交叉等等。
而对于不通明物体,则是分区块排序。在一个区块儿外部,物体绘制程序跟与相机的间隔无关。这么做次要因为严格依照间隔排序,不利于合批,合批须要优先思考材质、模型是否统一,而不是与相机间隔的远近。
四、PreZ pass/Depth prepass 是否有必要
PreZ pass 就是事后应用非常简单的 shader(开启 ZWrite,敞开色彩写入)画一遍场景,失去最终的 Depth Buffer。而后再应用失常 Shader(敞开 ZWrite,ZTest 批改为 EQUAL,不执行 clip),来进行绘制。这样只有最终显示在最下面的像素会绘制进去,其余像素都会因 EarlyZ 被剔除掉。
PreZ 的益处是升高 overdraw。害处是多画了一遍场景(尽管应用的是最简略的 shader),DrawCall 翻倍,顶点翻倍(顶点着色器执行两遍)。因为通常咱们游戏的瓶颈都在于像素着色器,所以大多数状况下 PreZ 都是有优化成果的。
PC 平台应用 PreZ pass 或者是个很好的抉择。一方面因为它没有挪动平台的各种隐面剔除技术,另外基于 IMR 渲染,对 DrawCall 和顶点数量不那么敏感。所以 PC 平台应用 PreZ pass 是很好的升高 overdraw 的伎俩。
挪动平台则恰好相反。挪动平台 GPU 都有各种隐面剔除技术,不通明物体自身就不存在 overdraw。而 TBR 架构对 DrawCall 和顶点数量异样敏感,大量顶点会导致更多的主存拜访,甚至会呈现主存 ParameterBuffer 放不下,产生 Flush 的状况。所以挪动平台不须要 PreZ pass。
如果理论测试发现,游戏的性能瓶颈在于 DrawCall 和顶点着色器,那么就不要应用 PreZ,这会进一步增大顶点压力。
回到咱们序言中提到的问题,咱们是否有必要对草地这样的物体应用 PreZ 呢?答案是有必要。草地是 AlphaTest,且有大量的交叉,不可避免的会有大量的 overdraw。通过 PreZ 能够很好的升高草地的 overdraw。同时,因为应用 PreZ 最初绘制草地的时候是不写深度的,也没有 clip,那么就能够当做不通明物体来绘制,不会像一般 AlphaTest 一样影响渲染管线的执行。
前面跟联发科工程师团队进一步沟通了解到,他们倡议去掉 PreZ pass 的起因是在测试咱们游戏的过程中发现,在特定场景下 vs 很容易呈现瓶颈,而 ps 反而留有余地。咱们下面提到,PreZ 会减少顶点数量,更加容易呈现 vs 的瓶颈。当然,理论游戏运行过程会非常复杂,可能略微换个视角或者换个手机就又是 ps 瓶颈了。如何取舍还是要以真机测试后果为准。
网上有一些文章提到,某团队应用 AlphaTest 代替 AlphaBlend 绘制草地,又或者某 2D 游戏,应用 AlphaTest 绘制角色,都取得了性能的大幅晋升。其起因就在于咱们下面所剖析的,当游戏的 overdraw 很重,或者 ps 是瓶颈的时候,应用 AlphaTest 能够利用 EarlyZ 做剔除,晋升性能。当然,这里要再次重申,肯定要确保不通明、AlphaTest、半透明,这样的绘制程序,如果 AlphaTest 或者半透明物体交叉到不通明物体之间绘制的话,会重大影响性能。
五、Shader 中的分支对性能的影响
-
分支对性能的影响
同一个 warp 内执行的是雷同的指令,当呈现分支(if-else)的时候,如果所有线程都走分支的一侧,则分支对影响很小。然而如果有些线程走 if 分支,有些线程走 else 分支。那么 GPU 的解决形式是,两条分支都走一遍,而后通过执行掩码(execution mask)抛弃不要的执行后果。这就带来了很多无意义的开销。这种状况就是咱们后面介绍的 Warp Divergence。
常量做分支条件,编译器会做优化,简直不会影响性能。
uniform 做断定条件,少数时候能够保障不会呈现 Warp Divergence,对性能也不会有太大影响。留神,并不能将不会有太大影响当做没有影响。应用分支的性能隐患有很多,下文还会具体阐明。
动静分支,如应用纹理采样的值做判断条件,大概率会产生 Warp Divergence,会重大影响性能,尽可能防止。
-
编译器对 shader 的优化
Unity 会应用 glsl optimizer 对 shader 做优化。少数时候会明显提高 shader 的执行性能。不过对于 if-else,它的默认解决形式是将分支开展,全副计算一遍,依据判断条件取其中一个后果。除非分支中的指令很简单,或者有大量纹理采样,它才会保留分支代码。这个行为是能够通过 branch 和 flatten 关键字来管制的。Unity 中的 UNITY\_BRANCH 和 UNITY\_FLATTEN 就对应这两个关键字。
branch,shader 会依据判断语句只执行当前情况的代码。
flatten,shader 会执行全副状况的分支代码,而后再依据判断条件取得后果。
unroll,for 循环是开展的,直到循环条件终止。
loop,for 循环不开展。
-
分支的性能隐患
大量 if-else 会导致 shader 指令数比拟多,占用的寄存器就更多。这会导致 GPU 的 Active warp 数量升高,升高 GPU 暗藏提早的能力。也有可能因为寄存器占用过多,产生 Register spilling(将寄存器写入主存)。
还有可能因为 shader 指令比拟多,导致编译后的 bin 文件比拟大,不利于缓存。
另外有的时候,咱们有一些计算或者纹理采样是无心中写在分支之外的,而计算结果只有分支中才会用得上。这种状况如果是 multi\_compile,则编译器会主动做优化,精简掉不必要的代码。然而如果是 if 分支,则编译器不会优化这块儿的代码。这可能导致执行了比预期要多的指令或者进行了不必要的纹理采样。
动静分支(包含 uniform 分支),可能会不利于驱动进行优化。具体驱动实现过于黑盒,而且随着驱动的迭代更新可能会不断改进。然而通常来说,如果咱们通过分支来替换 multi\_compile,都会减少 shader 的执行开销。如果是罕用 shader,或者游戏的 GPU 曾经跑的比拟满了,则分支的副作用不可疏忽,尤其是在低端机上。
某些驱动(常见于低端机),可能会在驱动级别对分支做“优化”,如果分支指令较少,会强制开展。然而分支的另外一个隐性开销在于参数传递导致的带宽减少。即使分支指令很少,带宽的减少也可能会成为压死骆驼的最初一根稻草。
Adreno3xx、4xx 局部驱动存在兼容性问题,应用 half 参数做断定条件执行 if 指令,永远无奈断定为真。批改为 float 做断定条件即可。所以,兼容性测试也须要多加关注。
-
multi\_compile 的副作用
如果不应用 if-else,那么另外一个抉择就是 multi\_compile。遗憾的是,应用 multi\_compile 同样会有显著的副作用。
1. 减少了 keyword 的数量,keyword 自身是无限的。
2. 减少了变体的数量,不同的变体其实是不同的 shader,这会导致 SetPassCalls 减少,影响运行时性能。
3. 变体增多会产生更多的内存占用。而且 Shader 理论的内存占用可能会比咱们在 UnityProfiler 中看到的 ShaderLab 的内存占用更多。因为驱动会耗费两三倍的内存去治理 ShaderProgram。相比减少大量变体,抉择 if-else,并应用 const 或者 uniform 作为其断定条件有的时候是更加现实的抉择。
-
对于分支的倡议
1. 尽量不要应用分支,如必须应用的话,优先选择常量的断定条件,其次抉择 uniform 变量作为断定条件。
2. 最蹩脚的状况是应用 shader 外部计算的值作为断定条件,尽可能防止。
3. 尽可能防止在罕用 shader,或者给低端机应用的 shader 中应用分支。
4. 最终确定要应用分支,请确保两条分支不存在大量反复代码。大量反复代码会导致 shader 占用寄存器文件显著增多,缩小 active warp 数量,最终导致性能降落。
5. 如果分支指令较多,不要遗记增加 branch 关键字。
6. 抉择分支还是 multi\_compile,肯定要以理论游戏性能测试为准。尤其要注意其对低端机 GPU 占用率的影响。
六、Load/Store Action 和 Memoryless
-
Load/Store Action
从 SystemMemory 拷贝数据到 TileMemory 是 Load Action。
从 TileMemory 拷贝数据到 SystemMemory 是 Store Action。也称为 Resolve。
OpenGLES 中能够通过 glInvalidateFramebuffer 来躲避上述 Load 和 Store。
Metal 中能够通过 RenderPass 的 loadAction 和 storeAction 的设置来管制 Load/Store。
loadAction 有三种,dontCare,load,clear。
storeAction 有四种,dontCare,store,multisampleResolve,storeAndMultisampleResolve。
比方后处理执行结束之后深度就没用了,那么就能够设置 depthTexture 的 RT 的 storeAction 为 dontCare。这样能够防止深度写回主存的带宽开销。
在 XCode 中能够看到渲染的 L/S bandwidth 的开销。通过抓帧能够分明的看到每个 RenderPass 的 load/store action。这能够很不便的帮忙咱们优化渲染管线的性能。
-
Memoryless
像 Depth/Stencil buffer,只在 Tile 绘制中有用,不须要存到主存中。所以其 storeAction 能够申明为 dontCare。这样能够节俭带宽。
Metal 下,这样不须要 Resolve 的 RT,能够设置为 Memoryless,这样能够升高显存开销。
-
Render Target 切换
上图能够看到,RenderTarget 的切换是十分慢的。在咱们游戏的渲染流程中,应该尽可能的防止频繁的 RT 切换。
因为挪动平台 TBDR 的个性,切换 RT 在挪动平台上会有更大的开销。它会重大阻塞渲染流水线的执行。每次切换 RT 都须要期待后面的指令全副执行结束,把数据写入主存。切换到新的 RT 后,还须要把数据从主存 Load 到 TileMemory 中。频繁的与主存交互不仅很慢,而且耗费大量带宽。所以相似后处理这样必须应用 RT 的,该当把多个 Pass 尽可能合并成一个 Pass。
当咱们应用 RenderTexture 的时候,肯定要谨慎。一方面它耗费了过多的内存。另外方面 RenderTexture 的绘制更新不可避免的会有 RT 切换。如果过多应用,或者过于频繁的更新,会呈现显著的性能问题,尤其是在低端机上。当确定要应用 RenderTexture 的时候,肯定要严格控制其大小。
一个咱们常见的性能,将场景或者角色模型绘制到一个 RenderTexture 上,而后将这个 RenderTexture 绘制到 UI 上。这个过程其实就存在显著的性能隐患。另外,有一些 UI 框架会做优化,将动态不变的 UI 绘制到一个 RenderTexture 上来缩小 DrawCall。如果不能保障 UI 真的齐全静止不动,在挪动平台上这么做通常是负优化。起因同样在于内存耗费和 RT 切换开销。
-
防止 CPU 回读 GPU 数据
CPU 回读 GPU 数据(比方 glReadPixels)会重大妨碍 CPU 和 GPU 的并行。当 CPU 要读取 FrameBuffer 中的数据的时候,必须要保障 GPU 曾经全副写入结束。
局部解决办法是,在下一帧的时候读取上一帧的数据。这样能够躲避期待的开销,不过毕竟是两帧数据,所以后果可能会有偏差。
-
Pixel local storage
Mali 和 Adreno 是提供了 API 来获取 TileMemory 中的数据。这样就能够高效的实现一些特殊效果,比方软粒子或者一些后处理成果。详情能够参考 Pixel Local Storage on ARM Mali GPUs。
iOS 平台因为应用了 Memoryless,framebuffer\_fetch 无奈获取深度数据,然而能够通过一些其余伎俩,比方 MRT(Multiple Render Targets),或者把深度写入到 color 的 alpha 通道来实现相似性能。
-
挪动平台提早渲染优化
传统提早渲染的 GBuffer 会带来大量的带宽开销,挪动平台上同样能够利用 OnChip Memory 来实现基于挪动平台的高性能提早渲染。能够参考挪动端渲染管线实现与优化: 带宽和功耗优化。
而原神貌似间接应用传统的提早渲染计划,并没有针对挪动端做性能优化,所以它只能在高配手机上能力跑得动。带宽的开销可见一斑。这里能够参考探秘《原神》的挪动端渲染技术。
七、MSAA 对性能的影响
-
MSAA
挪动平台的 MSAA 能够在 TileMemory 上实现 Multisampling,不会带来大量的拜访主存的开销,也不会大幅减少显存占用。所以挪动平台 MSAA 是比拟高效的。通过指定 FrameBuffer 格局就能够开启 MSAA,不须要通过后处理等计划来本人实现 MSAA。
然而这并不是说 MSAA 在挪动平台就是收费的了。它仍然是有肯定开销的,所以也只能在高配手机开启 MSAA。
在 Adreno 上,GMEM 大小是固定的(256k~1M),而 Tile 大小跟 RenderTarget 的格局无关。如果开了 MSAA,Tile 会对应放大,这就导致产生更多的与主存的交互。开启 HDR 会有更大开销也在于此。具体能够参考挪动端 GPU 的运作个性与 UE4 半透特效性能优化计划。
Mali 和 PowerVR 因为 TileMemory 无限,关上 HDR 与 MSAA 须要更多空间来保留渲染后果,GPU 只可能通过放大 Tile 的尺寸来适应 On-Chip Memory 的固定大小。进行渲染的 Tile 数量会因而而减少。比方 PowerVR 本来 Tile 是 32×32,如果开启 MSAA 可能就变为 32×16 或者 16×16。上面的表格显示了 Mali Bifrost GPU,bits/pixel 和 Tile 大小的关系。
Family | 16×16 Tile | 16×8 Tile | 8×8 Tile |
---|---|---|---|
<= Bifrost Gen 1 | 128 bpp | 256 bpp | 512 bpp |
\>= Bifrost Gen 2 | 256 bpp | 512 bpp | 1024 bpp |
- Alpha to coverage
Alpha-to-coverage 简略了解就是基于 MSAA,应用 AlphaTest 来模仿 AlphaBlend 的成果。本来的 AlphaTest 不可避免的会有比拟硬的边缘,通过 Alpha-to-coverage,像素的透明度是由 4 个像素插值计算得来的,边缘就会柔和很多。
应用 Alpha-to-coverage 的益处是,因为它的实质是 AlphaTest 也就是不通明物体,所以不会有渲染半透明物体那样容易呈现深度谬误的状况。
应用 Alpha-to-coverage 也会影响 EarlyZ 的执行(无论是 AlphaTest 影响还是 MSAA 影响),在挪动平台上性能不高,所以倡议只在必要的中央应用。
因为 Alpha-to-coverage 是基于 MSAA 的,所以不应用 MSAA 的时候,启用 Alpha-to-coverage,其后果是无奈预测的。不同的图形 API 和 GPU 对这种状况有不同的解决形式。
-
Shader 的优化倡议
MAD(乘加)是一条指令。将计算转换为 (a x b + c) 的模式,能够节俭指令。
saturate, negation, abs 是收费的。clamp, min, max 不是。不要进行负优化。
sin, cos, log, sqrt, pow, atan, atan2 应用 SFU 进行计算,通常须要破费几个 ALU 甚至几十个 ALU,尽可能防止。
CVT (类型转换,如 half–>float,vec3–>vec4)并不一定是收费的,可能须要占用一个 cycle。须要缩小无意义的类型转换。
先计算 scalar,再计算 vector,性能更好。通常编译器会做这个优化,然而编译器不肯定能优化到极致。在保障代码清晰的前提下,尽可能编写高效代码是个良好习惯。
优先应用半精度的浮点数(half),速度更快。lowp 和 mediump 是 FP16,highp 是 FP32。个别顶点坐标须要 FP32,其余如色彩都能够是 FP16。
不要单纯为了性能把标量合并为向量,尤其是以升高代码可读性为代价。这对古代 GPU 而言通常不会有优化成果,GPU 执行的时候还是拆散为多个标量来执行。手工合并数据很容易因为代码凌乱产生负优化,比方引入了隐式类型转换产生了更多的开销。
是否能够应用分支,参考下面对于分支性能的剖析。一个稳当的倡议是,要浏览编译后的代码,要在不同机型上在实在游戏环境下做测试。确保批改不是负优化。
在 PC 平台,应用 discard 剔除像素或者有优化成果,能够防止 ROP 的开销。在挪动平台则不要这么做,会影响 HSR 和 EarlyZ,是负优化。
缩小寄存器的应用,防止 Register Spilling(寄存器超过限度写入主存)。这里次要就是缩小 uniform 变量、长期变量的数量(简略说就是尽可能精简 shader 代码)。查看编译后的代码能够比拟清晰的看到这些。应用 Mali offline compiler 也能够定量化的取得寄存器数量和指令数据。
结语
理解 GPU 硬件架构和运行机制对咱们的性能优化工作有指导意义,能够帮忙咱们更快的剖析出游戏的性能瓶颈。比方下图是 SnapdragonProfiler 中的 Trace 截图。如果用 Mali 的 Streamline 的话能够看到更加具体(简单)的参数指标。如果对 GPU 不够理解的话,这些参数就毫无意义。
粉丝福利,后盾回复“GPU”取得本篇作者举荐相干学习材料
腾讯工程师技术干货中转:
-
快珍藏!最全 GO 语言实现设计模式【下】
-
如何成为优良工程师之软技能篇
-
如何更好地应用 Kafka?
-
从鹅厂实例登程!剖析 Go Channel 底层原理