目录
- Arm GPU 架构说明
- 移动系统的特点
- 渲染管线
- 渲染管线简介
- 几何处理
- 像素处理
- 渲染管线的硬件
- IMR(立即渲染)
- TBR(Tile Based Rendering)
- 渲染硬件的实现
- CPU
- GPU 设计
- Mali Shadercore
- 重要补充
Arm GPU 架构说明
UtGard:
比较早的架构,支持到 OpenGL-ES 2.0,VertexShader 和 FragmentShader 是分离的
主要型号:mali-200, mali-400, mali-450, mali-470
MidGard:
比较早的架构,基于 128 bit 向量
主要型号:mali-T6xx, mali-T7xx, mali-T8xx
Bifrost:
对于Bifrost,例如G76,一个shader core可以同时运行几十个线程,
从 Mali 的资料显示,shader core 一般主要由三个部分组成,ALU,L/S Uni,TEXTURE, late-Z unit 几个主要模块
在G76上是 8-wide wrap 的,一般设置为 3 个 ALU
(其余的型号可能不一样,例如 G51/G72 是 4-wide wrap 的,G72同样是3个ALU;
G52 跟 G76一样,不过G52可配置成2个ALU的)
主要型号:mali-G31, mali-G51, mali-G71, mali-G52, mali-G72, mali-G76
ValHal:
2018 推出,基本都是超标量处理器
主要型号:Mali-G57,Mali-G77
移动系统的特点
桌面系统和移动系统的区别在于,桌面系统由 CPU, GPU+VRAM, DRAM 组成,功耗在 225~370 W 左右
并且辅以主动散热;而移动系统使用统一内存,一块芯片上集成了 CPU, GPU, DRAM,功耗只有 2~3 W,被动散热,
并且手机的塑料或玻璃外壳并不利于散热,还有手机壳会影响散热。
因此手机渲染的效率要求是比桌面高很多的。(注:这也是为什么同样的芯片,在平板上性能可能会比手机更好)
移动 GPU 和其它主要硬件一般通过总线相连。移动 CPU 通常采用大小核设计,大核考虑性能,小核考虑功耗,系统会自动为线程分配合适的核心。统一内存被所有芯片共享,访问内存的操作也是极其昂贵的,约 100 mW/GB/s。DPU 可以将多层画面合为一帧,包括图像变换,颜色转换,根据显示帧率实时运行,一些复杂的合成任务仍可能会交由 GPU。
总线上除了上述模块,还有像是基带芯片、VPU、ISP、NPU、广播 I/O、相机 I/O 等,同样共享 2~3 W 的功耗。
提到功耗,就不得不提处理器的功耗曲线,提升性能的同时,功耗大约是指数上升的。基准频率下,处理器的功耗和性能比较均衡。手机要根据 GPU 或 CPU 的预算,为他们分配合适的功耗,但总数依然不超过 2~3 W。CPU 和 GPU 通过大量的并行计算,可以以较低的功耗获得一致的性能。(注:这里的假设是并行 Overhead 不大,且所有过程都可以并行化,实际情况会有一个最合适的并行数量)
渲染管线
渲染管线简介
以绘制地球月球为例子,CPU 把绘制的 Shader、Buffer、贴图以及 Attachment 准备好,所有需要的数据写入显存,然后发送了两个 Draw Call D1, D2。
GPU 几何处理调用 Vertex_Shader S1,计算出顶点位置,像素处理调用 Pixel Shader S2 对每一个像素进行着色。Shader S1, S2 和 Vertex Buffer B1 是复用的,但是传的参数不同,贴图 T1, T2 不同,所以绘制出来的东西也不同。(注:渲染管线的定义有很多,建议参照《Real-Time Rendering》一书的定义,这里的管线应该更偏向 API)
Compute 处理游离于渲染管线之外,它从显存(内存)中读取数据,然后又写回显存(内存)
几何处理
按顺序有 5 个阶段,输入是物体空间的顶点,输出是屏幕空间的图元:
顶点着色(可编程):执行 Vertex Shader(VS),一般计算骨骼动画、坐标转换
细分着色(可编程,可选):用于曲面细分增加细节,由 Controller Shader, Tessellator, Evaluation Shader 构成(Tesllation)
几何着色(可编程,可选):程序化生成和删除面片
图元组装:将顶点组装成点、线、三角面 (primitives)
图元剔除:剔除视锥体之外和背向 / 正向相机的图元 (clip & cull)
移动渲染通常不会使用细分着色,因为代价比较昂贵;
几何着色已经过时,一般可以用 Compute Shader 代替。
图元剔除可以发生在更早的阶段来节省开销,Mali 的 Bifrost 系 GPU Compiler 可以拆分 Vertex Shader,将其它属性的计算 (varying shader) 放到了剔除阶段之后。(也就是图中的 vertex_shader和varying Shader) 尽管 GPU 可以这么做,最好的方式是 CPU 在软件处理时就已经做了部分剔除。(注:比如逐物体的视锥体裁剪和软件遮挡剔除,桌面端还会用上 Hi-Z)
这里要注意移动 GPU 在图元装配之后,光栅化之前就开始了 primitives culling
未优化的渲染管线是 图元装配之后进行图元剔除,优化后的管线在拆分Vertex Shader称为 Vertex Shader 和 Varying Shader, 优先执行的是 Vertex Shader,然后在 Varying Shader 之前执行 primitives)
像素处理
像素处理也有 5 个阶段,输入是屏幕空间图元,输出是像素颜色:
光栅化(rasterazation):从图元到像素点到 Quad,有一些还支持可变渲染率,
Quad 为每四个像素一组(不同的 GPU 可能有区别),用于计算 Mipmap 和各向异性过滤需要的梯度 (四个像素为一组的集合)
Early ZS(Z & Stencil)测试:不修改像素 Depth/Stencil 的片元在此阶段选择性剔除
片元着色(可编程):执行 Pixel Shader(ps),计算像素颜色
Late ZS 测试:其余未经过 Early ZS 测试的片元在此阶段选择性剔除
混合(blend):混合颜色,体现透明或不透明物体
渲染管线的三个大阶段可以并行,以最大化压榨硬件性能。需要注意的是 OpenGL ES 的一些同步函数,例如 glFinish 或者 Vulkan 的 Barrier 设置不合理,会导致并行性下降。并行渲染的性能也具有木桶效应,三者中最耗时的部分将成为渲染瓶颈。
渲染管线的硬件
桌面GPU 硬件架构形式:几何处理 → 像素处理 → Framebuffer,通过一些 Buffer 来传递中间结果。可编程的 Shader Core 用于执行 Shader 代码,与 CPU 设计迥异。
现代 GPU 有两种常见的渲染模式:立即渲染(IMR)、Tile-Based 渲染(TBR)。
IMR(立即渲染)
立即渲染模式中,几何处理 → 像素处理使用的是简单的 FIFO Buffer,这块 Buffer 在显存中,Draw Call 以符合直觉的方式,顺序绘制到 Framebuffer 上。缺点是,GPU 读写内存中 Framebuffer 的带宽需求比较高。(注:尤其是延迟渲染,除非 Framebuffer 全部塞进显存里)
TBR(Tile Based Rendering)
Tile-Based 渲染中,Framebuffer 会被划分为一个个瓦片(Tile),每次仅渲染一个 Tile
几何处理 → 像素处理使用内存中的 Tile Lists 记录每个 Tile 需要的几何和渲染信息,处理完所有的三角面后,把每一个 Tile List 读入片上缓存,像素渲染在 GPU 中执行,最后将渲染结果传回内存。TBR 架构下,三角面片更昂贵,而渲染像素更高效。(可以看到是将 Geometry list 放到内存上了)
注意:TBR 和 TBDR 的区别在于几何处理的数据到像素处理数据的传递
由于统一内存读写非常昂贵,TBR 减少了大量带宽需求,提高了缓存的 Locality,使得 ZS 测试、MSAA、拷贝、面片剔除等操作的开销变得很小,并且可以实现完全的片上延迟渲染。缺点是 Tile Lists 位于内存中,三角面数、VS 的输出限制较高,无法高效执行细分着色和几何着色。
TBR 每个 Pass 三个阶段是严格顺序执行的,不同 Pass 之间可以并行。首先并行粒度就不是 Draw Call 而是 Pass,也容易遇到 API 的不并行问题。
移动 GPU 的 TBR 设计正是适应了高分辨率 2D 渲染的应用场景:面片不多,占屏比小,低功耗。
一些系统可能会采用介于 IMR 和 TBR 两者之间的渲染模式,取决与如何权衡顶点和像素开销。(注:猜想随着硬件和渲染 API 的发展,GPU 的最终目标是完全的高性能片上渲染)
渲染硬件的实现
CPU 和 GPU 的设计差别:
CPU | GPU |
---|---|
低线程数 | 极高线程数 |
注重单线程性能 | 注重计算的整体吞吐率 |
对内存延迟敏感 | 吞吐率取决于硬件能效 |
CPU
CPU 的一般的运行流水线是,取出指令 → 读寄存器 → 计算 → 写寄存器,其中 “读-算” 为一个 CPU 周期(clock),它需要花费多个时钟周期(常见的是 5~10 个 cycles),如果能减少每个 CPU 周期的时钟周期,就能在同样的频率下,获得更好的性能。前后指令之间经常有依赖关系,如果让流水线停下等待上一条指令执行完毕,会对性能造成影响。
CPU 处理器硬件优化:
-
结果转发(Result Forwarding):前一个指令的结果在写入寄存器前,即可作为后面指令的输入
-
分支预测:运行时等待会对性能造成影响,CPU 还可以猜测条件分支的走向,提早运行预测的路径,如果猜测正确,CPU 流水线的效率得到提高,若猜测错误则进行回滚
-
乱序执行:当不存在数据依赖时,很多指令能细粒度地并行,CPU 可以调用多组 ALU 来同时运行,前提是这些指令必须是连续的,因此高端处理器在取出指令后,会重新组合再做超标量(Super Scalar)分发
-
推测执行(Speculative Execution):在空闲的单元上,执行可能需要执行的指令,比如同时执行 if 和 else 指令
-
推测数据预读(Speculative Data Fetch):预测并提前在缓存中读入可能需要的内存数据
-
大容量缓存:比如 L2、L3 缓存,来应对内存延迟
如此多的组件只是为了让 CPU 的单线程性能提升,但它们同样会占据芯片空间,也会增加功耗。基于能效考虑,GPU 可以学习的是 CPU 中的 SIMD(单指令多数据流)的硬件设计。
GPU 设计
GPU 渲染核心的管线一样是,取出指令 → 读寄存器 → 计算 → 写寄存器,其特性在于向量化计算,同时会有多组 ALU 参与相同的计算。一个 “读-算-写” 的 GPU 周期会占据多个时钟周期,打个比方每过 10 个 cycles,GPU 将多个线程的指令和数据读入(注:多个线程执行的是相同的指令),然后同时计算。
渲染核心硬件优化:
这样设计的管线,每个周期内只进行一次计算操作,对单个线程来说是变慢了,但是保证了一定的吞吐量,良好的并行性避免了和 CPU 一样的需要乱序执行和推测执行。GPU 周期之间没有重叠,可以保证指令之间满足依赖条件,不需要结果转发和分支预测。
并行设计虽然对单线程的延迟不那么敏感,但与内存传输数据依然需要 ~150 个 GPU 时钟 cycles,而缓存也无法容下所有的渲染资源。GPU 的优势就在于线程数量极多,一部分线程向内存请求数据的同时,另一部分被唤起执行。所以数据预读和大容量缓存,在 GPU 面前也可能是多余的。
对于需要处理 RGBA 颜色或是 XYZW 矢量的 GPU 来说,SIMD 一次计算四个分量看上去很高效,但是还有大量的计算不能打包成 vec4,因此相比使用硬件矢量计算单元,SIMT(单指令多线程)是一种更具性价比的方案。
GPU 中执行相同指令的一组线程有很多名称,比如 Warp(nvidia), Wavefront(AMD), Subgroup quad(Arm)等。使用 SIMT 的好处的一个 Warp 的向量容量可以是变化的,多个线程共同分摊控制面 (Control Plane)开销。
坏处是同一个 Warp 在执行分支语句的时候,如果有一部分需要执行 if 而另一部分执行 else,所有的线程都需要运行两个分支,然后过滤掉不需要的部分结果。这叫 Warp Divergence,只能通过 Shader 代码去避免。(注:实际情况中动态分支如果能控制好 Divergence,可以达到一定的正面优化效果)
最后需要说明,GPU 有硬件加速单元,可以对插值、纹理解压、纹理过滤、颜色混合等固定操作进行加速,以此提升能效。
SIMT : 执行相同指令的一组线程;
SIMD : 单个指令可以执行多个数据的运算;
Mali Shadercore
一个 Mali GPU 的渲染核心大概长这样,渲染核心没有标准定义,不同厂商的设计也不尽相同。
ShaderCore 的内部逻辑如下:
重要补充
我在简书上的 GPU 知识博客:
GPU 基础知识