相关问题与回答
下列问题回答建立在目前对图形学的浅薄理解,以及阅读完参考文章与课程的基础上,不一定正确,如有错误👏🏻欢迎指出
GPU是如何与CPU协调工作的?
大体流程如下图:
-
将主存的处理数据复制到显存中。
-
CPU指令驱动GPU。
-
GPU中的每个运算单元并行处理。此步会从显存存取数据。
-
GPU将显存结果传回主存。
从硬件角度上分析:
- CPU与GPU的交流就是通过MMIO进行的。CPU 通过 MMIO 访问 GPU 的寄存器状态。
- DMA传输大量的数据就是通过MMIO进行命令控制的。
- I/O端口可用于间接访问MMIO区域
- CPU发出的命令流(command stream)被提交到硬件单元,也就是GPU Channel
- 每个GPU Channel关联一个context,而一个GPU Context可以有多个GPU channel
从程序开发角度:
- 我们会编写与C++等语言类似的用于GPU的语言(GLSL、HLSL、CGSL),也就是shader
- 在CPU通过编译器将shader转成面向机器的二进制指令(二进制指令可转译成汇编代码,方便阅读调试)
- CPU端将shader二进制指令经由PCI-e推送到GPU端
- GPU在执行代码时,会用Context将指令分成若干Channel推送到各个Core的存储空间
- 补充一点:CPU和GPU存在是否共享内存的两种架构–分离式和耦合式,分离式如上诉所说用PCI-e总线通讯,耦合式会有一些小差异
GPU也有缓存机制吗?有几层?它们的速度差异多少?
和CPU类似,也有多级缓存机构,同理也有Cache Miss的问题
课上提到Mipmap一方面是为了解决纹理下采样闪烁问题(即远处由于近大远小的关系一个像素可能匹配纹理多个纹素,导致像素与像素直接差距巨大),另一方面纹理读取时会多读取周围部分,如果差距过大,容易cache miss,需要重新去读取纹理,从而增加带宽消耗。
一般为这几层缓存:寄存器、L1缓存、L2缓存、GPU显存、系统显存。
储存类型 | 寄存器 | 共享内存 | L1缓存 | L2缓存 | 纹理、常量缓存 | 全局内存 |
---|---|---|---|---|---|---|
访问周期 | 1 | 1-32 | 1-32 | 32-64 | 400-600 | 400-600 |
GPU的渲染流程有哪些阶段?它们的功能分别是什么?
从渲染流水线看(具体作用看之前笔记):
应用阶段-几何阶段-图元装配阶段-光栅化阶段-逐片元操作-后处理阶段
从GPU逻辑管线看(以Fermi家族为例):
- 程序通过图形API(DX、GL、WEBGL)发出drawcall指令,指令会被推送到驱动程序,驱动会检查指令的合法性,然后会把指令放到GPU可以读取的Pushbuffer中。
- 经过一段时间或者显式调用flush指令后,驱动程序把Pushbuffer的内容发送给GPU,GPU通过主机接口(Host Interface)接受这些命令,并通过前端(Front End)处理这些命令。
- 在图元分配器(Primitive Distributor)中开始工作分配,处理indexbuffer中的顶点产生三角形分成批次(batches),然后发送给多个GPCs。这一步的理解就是提交上来n个三角形,分配给这几个GPC同时处理。
- 在GPC中,每个SM中的Poly Morph Engine负责通过三角形索引(triangle indices)取出三角形的数据(vertex data),即图中的Vertex Fetch模块。
- 在获取数据之后,在SM中以32个线程为一组的线程束(Warp)来调度,来开始处理顶点数据。Warp是典型的单指令多线程(SIMT,SIMD单指令多数据的升级)的实现,也就是32个线程同时执行的指令是一模一样的,只是线程数据不一样,这样的好处就是一个warp只需要一个套逻辑对指令进行解码和执行就可以了,芯片可以做的更小更快,之所以可以这么做是由于GPU需要处理的任务是天然并行的。
- SM的warp调度器会按照顺序分发指令给整个warp,单个warp中的线程会锁步(lock-step)执行各自的指令,如果线程碰到不激活执行的情况也会被遮掩(be masked out)。被遮掩的原因有很多,例如当前的指令是if(true)的分支,但是当前线程的数据的条件是false,或者循环的次数不一样(比如for循环次数n不是常量,或被break提前终止了但是别的还在走),因此在shader中的分支会显著增加时间消耗,在一个warp中的分支除非32个线程都走到if或者else里面,否则相当于所有的分支都走了一遍,线程不能独立执行指令而是以warp为单位,而这些warp之间才是独立的。
- warp中的指令可以被一次完成,也可能经过多次调度,例如通常SM中的LD/ST(加载存取)单元数量明显少于基础数学操作单元。
- 由于某些指令比其他指令需要更长的时间才能完成,特别是内存加载,warp调度器可能会简单地切换到另一个没有内存等待的warp,这是GPU如何克服内存读取延迟的关键,只是简单地切换活动线程组。为了使这种切换非常快,调度器管理的所有warp在寄存器文件中都有自己的寄存器。这里就会有个矛盾产生,shader需要越多的寄存器,就会给warp留下越少的空间,就会产生越少的warp,这时候在碰到内存延迟的时候就会只是等待,而没有可以运行的warp可以切换。
- 一旦warp完成了vertex-shader的所有指令,运算结果会被Viewport Transform模块处理,三角形会被裁剪然后准备栅格化,GPU会使用L1和L2缓存来进行vertex-shader和pixel-shader的数据通信
- 接下来这些三角形将被分割,再分配给多个GPC,三角形的范围决定着它将被分配到哪个光栅引擎(raster engines),每个raster engines覆盖了多个屏幕上的tile,这等于把三角形的渲染分配到多个tile上面。也就是像素阶段就把按三角形划分变成了按显示的像素划分了。
- SM上的Attribute Setup保证了从vertex-shader来的数据经过插值后是pixel-shade是可读的。
- GPC上的光栅引擎(raster engines)在它接收到的三角形上工作,来负责这些这些三角形的像素信息的生成(同时会处理裁剪Clipping、背面剔除和Early-Z剔除)。
- 32个像素线程将被分成一组,或者说8个2x2的像素块,这是在像素着色器上面的最小工作单元,在这个像素线程内,如果没有被三角形覆盖就会被遮掩,SM中的warp调度器会管理像素着色器的任务。
- 接下来的阶段就和vertex-shader中的逻辑步骤完全一样,但是变成了在像素着色器线程中执行。 由于不耗费任何性能可以获取一个像素内的值,导致锁步执行非常便利,所有的线程可以保证所有的指令可以在同一点。
- 最后一步,现在像素着色器已经完成了颜色的计算还有深度值的计算,在这个点上,我们必须考虑三角形的原始api顺序,然后才将数据移交给ROP(render output unit,渲染输入单元),一个ROP内部有很多ROP单元,在ROP单元中处理深度测试,和framebuffer的混合,深度和颜色的设置必须是原子操作,否则两个不同的三角形在同一个像素点就会有冲突和错误。
Early-Z技术是什么?发生在哪个阶段?这个阶段还会发生什么?
Early就是提前进行一次深度测试,减少overdraw的影响,提高渲染性能。
发生在光栅化与像素着色器之间。
Early-Z剔除的最小单位不是1像素,而是像素块(pixel quad,2x2个像素)
像素块的好处是能方便计算mipmap层级(通过ddx、ddy),同时也能精简SM的架构、减少硬件单元数量和尺寸。
但同时像素块可能会激化OverDraw的影响,比如下图三个有效像素却需要用到12个像素来绘制。
使Early-Z失效:
- 开启了AlphaTest
- shader里有discard/clip指令
- 关闭深度测试
Early-Z技术会导致一个问题:深度数据冲突(depth data hazard),可以再写入深度值之前再次与frameBuffer的值做一次对比来解决。
Early-Z不能完全避免overdraw,且容易失效,TBR移动架构上提出了HSR等手段来加强
SIMD和SIMT是什么?它们的好处是什么?co-issue呢?
SIMD: Single Instrument Multiple Data,单指令多数据;
一条指令可以处理多维向量的数据,原来的多条指令用一条指令即可处理完。
比如对向量进行运算,以前需要对xyzw每个维度一条指令
SIMT: Single Instrument Multiple Threads,单指令多线程。
单个SM中的多个Core同时处理同一指令,并且每个Core存取的数据可以是不同的即a、b 、c的值可以不一样。
在SIMD基础上增加了多Core支持
co-issue是为了尽可能充分利用SIMD,将低维向量合并成Vector4以提高ALU的利用率。
一次指令可以处理四个维度数据,但shader中可能只需要处理单个通道,使用co-issue可以合并多条指令,提高单指令利用率
GPU是并行处理的么?若是,硬件层是如何设计和实现的?
GPU就是为了并行处理而设计的。是基于数据的并行处理。
程序员编写的shader是在SM上完成的。每个SM包含许多为线程执行数学运算的Core(核心)。例如,一个线程可以是顶点或像素着色器调用。这些Core和其它单元由Warp Scheduler驱动,Warp Scheduler管理一组32个线程作为Warp(线程束)并将要执行的指令移交给Dispatch Units。
而从SIMD、SIMT等技术看都是方便并行计算而诞生的。
GPC、TPC、SM是什么?Warp又是什么?它们和Core、Thread之间的关系如何?
GPC: Graphics Processor Cluster,图形处理簇;
TPC: Texture Processor Cluster,纹理处理簇;
SM:Stream Multiprocessor,流多处理器;
Warp:线程束,GPU并行计算的最小粒度;
1个GPU可以有多个GPC,1个GPC可以有多个TPC,1个TPC可以有多个SM。
每个SM包含许多Core。他们由Warp Scheduler驱动,其Warp Scheduler管理一组32个Threads
顶点着色器(VS)和像素着色器(PS)可以是同一处理单元吗?为什么?
在早期的GPU,顶点着色器和像素着色器的硬件结构是独立的,它们各有各的寄存器、运算单元等部件。这样很多时候,会造成顶点着色器与像素着色器之间任务的不平衡。
为了解决VS和PS之间的不平衡,DirectX10引入了统一着色器架构(Unified shader Architecture)。用了此架构的GPU,VS和PS用的都是相同的Core。也就是,同一个Core既可以是VS又可以是PS。
像素着色器(PS)的最小处理单位是1像素吗?为什么?会带来什么影响?
像素块(pixel quad,2x2个像素)
在像素着色器中,会将相邻的四个像素作为不可分隔的一组,送入同一个SM内4个不同的Core。
像素块的好处是能方便计算mipmap层级(通过ddx、ddy),同时也能精简SM的架构、减少硬件单元数量和尺寸。
但同时像素块可能会激化OverDraw的影响,比如下图三个有效像素却需要用到12个像素来绘制。
Shader中的if、for等语句会降低渲染效率吗?为什么?
大多数情况会,
SM的warp调度器会按照顺序分发指令给整个warp,单个warp中的线程会锁步(lock-step)执行各自的指令,如果线程碰到不激活执行的情况也会被遮掩(be masked out)。被遮掩的原因有很多,例如当前的指令是if(true)的分支,但是当前线程的数据的条件是false,或者循环的次数不一样(比如for循环次数n不是常量,或被break提前终止了但是别的还在走),因此在shader中的分支会显著增加时间消耗,在一个warp中的分支除非32个线程都走到if或者else里面,否则相当于所有的分支都走了一遍,线程不能独立执行指令而是以warp为单位,而这些warp之间才是独立的。
动态分支即有条件不一致的情况,需要等所有分支线路走完,再根据条件输出结果,导致alu利用效率不高,如果warp内只有一个需要额外分支,就导致其他线程再等待计算。
而静态分支是虽然有if语句,但输入的数据一致,所有线程走向同一个分支,此时就不会降低效率。
如下图,渲染相同面积的图形,三角形数量少(左)的还是数量多(右)的效率更快?为什么?
三角形数量少的效率更快,顶点数目越多,携带的顶点数据也多,与CPU的交互也多(数据传输、指令)
同时更多的三角形也可能带来更多的overdraw
GPU Context是什么?有什么作用?
类似CPU中进程上下文,管理着运行指令和数据状态等。
当一组context因访问缓存或内存而延迟时(读取纹理等),调度器可以激活下一组context进行工作,从而避免堵塞,可以高效利用上ALU单元,越多Context可用就越可以提升运算单元的吞吐量。
造成渲染瓶颈的问题很可能有哪些?该如何避免或优化它们?
- CPU和GPU的数据交换
- 合批手段
- 减少顶点数、面数
- 避免每帧提交Buffer数据(动画、粒子系统)
- 启用GPU Instance
- LOD功能
- 使用SRP Batch减少set pass call
- overdraw问题
- 确保Early-Z生效
- 移动端使用HSR之类手段
- 减少透明物体使用
- shader执行效率
- 避免if、Switch等分支语句
- 避免for循环语句,特别是循环次数可变
- 减少特殊复杂数学函数调用
关键词
GPU架构中常有以下模块(忘了就多回顾文章)
- GPC
- TPC
- Thread
- SM、SMX、SMM
- Warp
- SP
- CORE
- ALU
- FPU
- SFU
- ROP
- Load/Store Unit
- L1 Cache
- L2 Cache
- Memory
- Register File
优化建议:
- 尽量使用自己扩展的几何实例化代替unity提供的静态合批,动态合批,前者将合并mesh增加的vbo内存占用,后者则会增加cpu端的耗时开销。
- 尽量去减少顶点数与三角形面数,前者减少顶点着色器的运算,另外减少gpu显存中frameData的内存存储,后者减少片段着色器的消耗
- 避免每帧提交buffer的数据,比如unity的cpu版本的粒子系统,可使用gpu版本的粒子系统,将修改数据移动至gpu端,另外特别提醒的就是尽量避免大片的透明粒子特效,这将造成严重的overdraw
- 减少渲染状态的设置与获取,例如在Update中获取设置shader的属性或者公共变量,因为前面我们说到cpu是通过mmio获取寄存器数据,这将耗费更多的时间周期
- 3D物体应使用LOD减少处理的顶点与面数消耗,开启mipmap减少贴图缓存命中的丢失
- 避免alphatest的使用,会造成earlyZ失效
- 避免三角面过小,这会加剧过度绘制的情况,也就是前面提到的一个三角形只占3个像素点,却使用了12个线程去计算像素值,然后遮蔽其余9个的计算结果。
- 在寄存器数量与变体中寻找平衡,使用if变量达成静态分支,取代变体,一方面可以减少变体数量,另一方面也可以使得URP中的SRP Batch更高效合批
- 避免动态的判断分支,也就是Shader计算中if true 和 false 都会走的情况
- 减少复杂函数的调用,因为我们从硬件架构上就可以看出特殊函数处理单元是远远少于正常计算的单元的
参考文章
- 课程地址:https://www.bilibili.com/video/BV1aM4y1g75f
- 课程PPT:https://docs.qq.com/slide/DUUJkRFZUbUhYeEdq
- 深入GPU硬件架构及运行机制:https://www.cnblogs.com/timlly/p/11471507.html
- 【《Real-Time Rendering 3rd》 提炼总结】(十二) 渲染管线优化方法论:从瓶颈定位到优化策略
- 计算机组成原理 — GPU 图形处理器:https://www.cnblogs.com/grj001/p/12223429.html
- NVIDIA GPU 架构梳理:https://zhuanlan.zhihu.com/p/394352476
- 大佬笔记1:https://www.yuque.com/xiaohen-ecwjj/vegbg9/xzfv8x#BP50l
- 大佬笔记2:https://www.cnblogs.com/anesu/p/15807749.html#/c/subject/p/15807749.html