当我们谈优化时,我们谈些什么

原创 作者:洛城 2019-06-12 2.5k

前言

过去几年里,我经历过大约几十场面试,几乎在每次面试的时候,面试官都会问提一个问题:“你在渲染性能优化方面有什么经验?”这个时候我就会开始揣测面试官的意图,试着去回忆他之前提的问题,看看面试官到底想听什么样的回答:往往这种尝试最后都是失败的,结果就是不知道从何说起。因为没有具体的情境,最后只能说“整个渲染流程中很多地方都可能出现性能瓶颈,只能case by case的去看,找到项目的具体瓶颈,然后针对性地去解决。”几乎所有听到这个回答的面试官都会对我意味深长地一笑,不置可否:一旦看到这种笑容,我就知道糟了。之后的面试反馈中,很多人对我的评价就是“对渲染算法比较熟悉,但是在性能优化方面经验欠缺”。

总得来说我觉得这不是一个好问题,因为太过宽泛而没有针对性。我并不想泛泛地说“减少模型数量,减少/合并draw call,缩减贴图尺寸,压缩贴图,使用LOD”,因为这就是所谓“正确但无用的话”:所有游戏不都是这么优化么?此外,对于一个项目来讲,模型的面数,贴图尺寸,LOD的级别这些信息往往是在DEMO阶段就已经由TA主导确立的。对于引擎程序员来讲,需要你提出优化方案的,通常是在项目的开发过程中产生的新瓶颈(当然你首先需要定位它)。但反过来,我的回答其实也一样是“正确的废话”:所有性能优化的流程不都是这样吗?

一个典型的性能优化的流程,从profile开始,到确定瓶颈,然后针对瓶颈优化,测试优化的效果,再进入下一轮的profile(一个性能的优化有可能会导致新的性能瓶颈产生),如此无限循环

所以,当我们谈论性能优化的时候,我们究竟在谈些什么呢?

我试着理解了这个问题的意图:如果我们换一种问法,比如“渲染常见的性能瓶颈有哪些?具体可能出现在什么样的情景下?为什么这些情景会造成对应的性能瓶颈?”会不会是一个更好的问题?所以这篇文章,是在试着回答这个新的问题。不同于以往的文章,优化本身确实是一个比较宽泛的主题,所以本文的组织也相对比较松散,很多内容可能是我想到哪儿写到哪儿。其中有些概念基于我对硬件的理解,如有错误之处,欢迎指正。

说说GPU的架构

核弹厂有一篇关于自家GPU架构和逻辑管线的非常好的文章[1],如果你想要对GPU的结构有一个比较完整系统的认识,请一定不要错过这篇Life of a Triangle。比较可惜的是,这篇文章只更新到Maxwell这代架构,没有较新的Pascal架构(GTX10x0系列)和Turing架构(RTX20x0)的技术细节。不过总体来说,现代GPU的设计架构已经趋于稳定,一般只是针对某些单元做优化,或者增加feature,所以文章中的大部分内容在这里仍然是有效的,这是文中的一张图:


这张图是基于数据的流向,对GPU的硬件单元进行了大致的划分,实际上GPU中,最核心的部件可以被分成三大块,我画了图来示意他们大致的协作模式:


通常来说,GPU会有三个比较重要的部分,分别是控制模块,计算模块(图中的GPC)和输出模块(图中的FBP)。通常来说,GPU架构的设计需要有可伸缩性,这样通过增加/阉割计算和输出模块,就能够产生性能不同的同架构产品(比如GTX1070和GTX1080的主要区别就在于GPC和FBP的数量),以满足不同消费水平和应用场景的需求。

控制模块

控制模块负责接收和验证(主要是Host和Front End)来自CPU的经过打包的PushBuffer(经过Driver翻译的Command Buffer),然后读取顶点索引(注意是Vertex Indices不是Vertex Attributes,主要由Primitive Distributor负责)分发到下游管线或者读取Compute Grid的信息(主要由CWD负责,这部分是Compute Pipeline,不作展开)并向下游分发CTA。

Tips:计算管线和图形管线共享大部分的芯片单元,只在分发控制的单元上各自独享(PD和CWD)。许多较新的Desktop GPU允许图形和计算管线并行执行,可以在一些SM压力轻的图形计算环节(比如Shadow Map绘制),利用Compute Shader去做一些SM压力重的工作(比如后处理),让各个硬件单元的负载更加平衡[2]。

计算模块

计算模块是GPU中最核心的部件,Shader的计算就发生在这里。早期的硬件设计上,我们会区分VS,PS等Shader类型,并设计专用的硬件单元去执行对应类型的Shader,但这样的方法并不利于计算单元满负荷运转,所以现在所有的GPU设计都是通用计算单元,为所有Shader类型服务。在NV的显卡里这个模块全称是Graphics Processing Cluster,通常一个GPU会有多个GPC,每个GPC包含一个光栅器(Raster)负责执行光栅化操作,若干个核心的计算模块,称之为Texture Process Cluster(TPC),关于TPC,我们进一步分解来看这张大图[3]:


通常来说,一个TPC拥有:

(1)若干个用于贴图采样的纹理采样单元(Texture Units)

(2)一个用于接收上游PD数据的Primitive Engine,PE作为一个固定单元,负责根据PD传来的顶点索引去取相应的顶点属性(Vertex Attribute Fetch),执行顶点属性的插值,顶点剔除等操作

(3)一个负责Shader载入的模块

(4)若干执行Shader运算的计算单元,也就是流处理器(Streaming Multi-Processor,SM,AMD叫CU)

TPC内最核心的部件就是SM,这里我们再进一步分解SM看这张大图:


一个SM通常拥有一块专用于缓存Shader指令的L1 Cache,若干线程资源调度器,一个寄存器池,一块可被Compute Pipeline访问的共享内存(Shared Memory),一块专用于贴图缓存的L1 Cache,若干浮点数运算核心(Core),若干超越函数的计算单元(SFU),若干读写单元(Load/Store)。

作为核心计算单元,GPU的设计思路和CPU有很大的不同,就我所知的体现在两个方面:

(1)GPU拥有较弱的流程控制(Flow-Control)的能力

(2)GPU拥有更大的数据读写带宽,并配合有更多样的延迟隐藏技术

GPU的执行模型

要详细解释这两点,我们就需要理解GPU的执行模型:GPU的设计是为了满足大规模并行的计算,为此,它使用的是SIMD(Single Instruction Multiple Data)的执行模式,在内部,若干相同运算的输入会被打包成一组并行执行,这个组就是GPU的最小执行单元,在NV叫做Warp,每32个thread为一组,在AMD叫做Wavefronts,每64个thread为一组。基于不同的shader阶段,被打包执行的对象会有区别,比如VS里,就是32个顶点为一组,PS里,就是8个pixel quad(2*2像素块)为一组。

那么GPU又如何处理分支呢?我们知道,CPU有一种经典的处理分支的方法,叫做分支预测[4]。CPU会根据一组数据之前的分支结果去预测下一次分支的走向,如果错误就会有额外的开销。GPU没有这么复杂的流程控制,它的流程控制基于一种叫做“active mask”的技术,简单来说就是用一个bit mask去判断当前32个thread的branch状态,如果是0,则表示只需要执行false的branch,如果bit mask是2^32-1,则表示只需要执行true的branch,否则就需要对某些thread执行true,同时另一些在执行true的同时等待这些thread,反之亦然,这种情形下一个warp的执行时间就是Max(branch(true))+Max(branch(false))。

GPU的内存类型

Desktop GPU的内存类型和CPU比较相似,也是多级缓存的机制,我们能够接触到的内存类型包括Register,Shared Memory(本质是L1 Cache的一块),Texture L1 Cache(本质是L1 Cache的一块),Instruction Cache(本质是L1 Cache的一块),L2 Cache,DRAM,各类存储器的容量在是依次增大的,相应的它们在芯片上的位置也是离核心单元SM越来越远,同时访存延迟也是逐级增大的。

关于Desktop的内存类型,包括更多的延迟隐藏技术是一个比较大的话题,这里无法再详细展开,可以去参考其他文献[5]。

对于GPU在这几种内存中的访存延迟,我从这篇文章[6]找到了一些数据:

给小白的tips:$表示cache,刚进NV的时候我也不知道这是啥意思

对于PC端存储器的速度,可以查看这个网站。

Mobile GPU没有专用的显存,而是和CPU共享同一块系统内存(缓存机制当然也应该是共享的),但它有一块位于GPU上的专用on chip memory,这里没有找到Mobile GPU上的延迟数据,如果有相关数据请告诉我。

GPU拥有大量的寄存器(数量远多于CPU),是为了能够快速的在warp之间做切换:当某个warp被某些指令阻塞的时候(比如贴图采样),warp schedular可以让其处于休眠状态,并且把shader core的资源让出来,唤醒那些未被阻塞的warp。对于CPU来说,context switch的开销来源于寄存器的恢复和保存(没那么多寄存器,只能复用),但是对于GPU,每个warp是独占一份自己的register file的,这样就可以几乎无消耗地切换warp。相应的,一个SM里能同时并行多少个warp,就取决于一段shader到底占用了多少register,占用的越多,则能够并行的warp就越少。

Tips:如果没有记错的话,一直到Turing之前的架构,同一个SM内都只能执行一个shader,新的Turing架构似乎是允许SM内执行不同shader。

这里补一张图简单说一下Turing架构它和上几代显卡在SM上的区别:


相较于上几代的GPU,Turing在SM中增加了专用于光线追踪的RTCore,以及用于张量计算的TensorCore(后者主要是用于深度学习。在Turing之后,你还可以在做Graphics的同时利用TensorCore去做一些DL的工作,比如DLSS[7]?好像没什么x用)。下面两张图简单解释了RTCore前后光线追踪的基本流程:



这个图看起来很复杂,其实很简单:对于非Turing架构来说,光线和BVH的遍历求交、光线和三角形的求交、光线和三角形交点的着色这三件事,都是翻译成了数千条SM的指令给FP Core执行的。而Turing架构则是把前两件事作为固定硬件单元集成在了RTCore里,所以RTCore核心功能有两个:遍历BVH和光线-三角形快速求交。


输出模块

输出模块(Framebuffer Partition,FBP)比较简单,最核心的部件是一个称之为ROP(Raster Operation)的单元,ROP又包含了两个子单元,分别是CROP(Color ROP)和ZROP,前者负责Alpha Blend,MSAA Resolve等操作,并把最终的颜色写到color buffer上,后者则负责进行Stencil/Z Test以及把depth/stencil写到z buffer上。

Pascal和Turing架构的补充

相较于Maxwell,Pascal架构在图形方面的feature主要针对VR渲染(Lens Matched Shading,Single Pass Stereo等),因为当时恰好是VR概念大火的一年,具体的技术细节可以参考这篇文章[8]。而Turing架构在图形方面最大的feature莫过于引入了实时光线追踪,针对VR和可编程管线部分也有比较进一步的优化,具体可以参见这篇文章[9]。有关光线追踪的技术,我在之前的两篇文章[10][11]中有比较详细的解释。

在工艺制程和硬件参数方面,这篇文章[12]也给了我们一些参考数据:



如果你希望对这些参数包括各类显卡的参数包括新特性有更深入的了解,也可以去试着读一读各代显卡的技术白皮书[13][14]。如果这些细节还不足以满足你对硬件的好奇心,那么强烈建议你去核弹厂工作。

Mobile GPU和Desktop GPU的差异

目前市面上主流的Mobile GPU生产商(Qualcomm,ARM,Imagination)的GPU架构都是由Desktop GPU发展而来,因此在硬件架构上同桌面级GPU差异不太大,值得一提是这几点[15]:

(1)Mobile GPU的芯片面积和功率远小于Desktop GPU,两张图:



(2)Mobile GPU位于SoC上,和CPU共享内存,SoC是整体供电的,没有专用的电源输出到GPU

(3)移动设备是被动式散热,整个SoC的供电基于Thermal Throttling机制,当设备功率过高发热过量时,电源会降低输送功率防止SoC过热,这意味着如果CPU负担过大触发这一机制,同样也会使得GPU的性能下降

对于低端Mobile GPU,通常限制性能的是芯片面积,而对高端Mobile GPU,限制性能的则是带宽和发热。

回到渲染管线

回到我们一开始说过的那句“无用的废话”:渲染管线的任何阶段都有可能成为性能瓶颈。那么如果你试图列举大部分可能的性能瓶颈,就首先需要对整个GPU的渲染管线比较熟悉,并且能够大致地知道GPU在渲染管线的每个阶段,大致都做了哪些事,我们的每一个Graphics API Call在GPU端又对应着什么样的行为?基于这样的“翻译”,我们才能够理解那些性能瓶颈产生的原因,也能够理解我们之前说到的那些“泛泛”的优化策略到底为什么能够解决一些性能上的瓶颈。

说到渲染管线,就必须介绍当前GPU使用的三种不同的渲染管线:Immediate Mode Rendering,Tile Based Rendering和Tile Based Deferred Rendering[16]。我们用三张图来详细描述IMR,TBR和TBDR三种模式的渲染管线:

IMR模式的管线


IMR模式的第一个阶段是Vertex Processing,这个环节包括从DRAM/System Memory取Vertex Indices(PD的工作),然后根据Vertex Indices去Vertex Buffer取相应的属性(PE中VAF的工作),需要注意的是,取Indices/Vertex Attributes的阶段都会有L2 Cache在工作,表示如果顶点短时间内被share多次,则可以通过cache命中减少加载时间。加载完顶点数据后,Vertex Shader将会被加载到SM的Instruction Cache,紧接着就是VS在SM的执行。

VS执行完毕后,PE内的固定单元会执行顶点剔除来剔除一些视口外的三角形,背面剔除也在这个阶段发生。

接下来,由Raster对三角形进行光栅化,光栅化完毕的像素将会被打包成warp,经过XBAR重新流入SM(可能是同一个SM,也可能是不同的SM)。重新进入SM的每个pixel会根据其重心坐标,使用PE内的固定单元进行属性插值,从而得到depth,varying attributes等信息。

对于没有Apha Test的pixel quad,由ZROP对其执行early-Z test。

对于通过early-Z test的像素,在SM内执行pixel shader。

对于开启Alpha Test的像素,由ZROP对其进行late-Z test,并根据结果决定是否更新FrameBuffer相应位置的颜色和深度值。

若需要更新,则ZROP根据depth test的设置更新z buffer,CROP根据blend的设置去更新color buffer。

注意,IMR的整个流程中,三角形是可以以Stream的形式逐步提交给管线的,先提交的三角形也不需要去等待同一个Render Target上的其他三角形。

有关IMR管线的描述,这篇slides[17]的描述比本文要详细很多,非常建议仔细阅读。

IMR是所有Desktop GPU的标配,因为Desktop GPU相较于Mobile GPU,有更多的带宽用于读写,有专用供电接口,也不受限于芯片发热的问题。IMR架构的好处是设计上会相对来说比较清晰简明,并且整个管线是连续的,draw call之间不需要互相等待,有利于最大化吞吐量。对于Mobile GPU来说,只有NV的Tegra系列是基于IMR的

TBR模式的管线


TBR架构的GPU会把整个逻辑渲染管线打断成两个阶段

第一阶段和IMR类似,它负责顶点处理的工作,不同的是在每个三角形执行完他们的VS之后,还会执行一个称之为Binning Pass[18]的阶段,这个阶段把framebuffer切分成若干个小块(Tiles/Bins),根据每个三角形在framebuffer上的空间位置,把它的引用写到受它影响的那些Tiles里面,同时由VS计算出来的用于光栅化和属性插值的数据,则写入另一个数组(我们可以认为图中Primitive List就是我们说的一个固定长度数组,其长度依赖于framebuffer划分出的tile的数量,数组的每个元素可以认为是一个linked list,存的是和当前tile相交的所有三角形的指针,而这个指针指向的数据,就是图中的Vertex Data,里面有VS算出的pos和varying变量等数据)。在Bining Pass阶段,Primitive List和Vertex Data的数据会被写回到System Memory里。

Tips:TBR的管线会等待同一个framebuffer上所有的三角形的第一阶段都完成后,才会进入到第二阶段,这就表示,你应该尽可能的少切换framebuffer,让同一个framebuffer的所有三角形全部绘制完毕再去切换

第二阶段负责像素着色,这一阶段将会以Tile为单位去执行(而非整个framebuffer),每次Raster会从Primitive List里面取出一个tile的三角形列表,然后根据列表对当前tile的所有三角形进行光栅化以及顶点属性的插值。后面的阶段TBR和IMR基本是一致的,唯一区别在于,由于Tile是比较小的,因此每个Tile的color buffer/depth buffer是存储在一个on chip memory上,所以整个着色包括z test的过程,都是发生在on chip memory上,直到整个tile都处理完毕后,最终结果才会被写回System Memory。

Tips:TBR的优化实际上是利用缓存的局部性原理。

TBDR模式的管线


TBDR和TBR模式基本类似,唯一的区别在于,TBDR模式在执行光栅化之后,不会急着shading,而是会对rasterized sample进行消隐(基于depth buffer和相同位置的其他sample深度去移除被遮挡的sample),这个消隐的过程结束之后,tile上剩下的sample才会被送到PS里面去做shading。

Tips:通常TBR/IMR模式的GPU是基于比较简单的early-Z reject去防止overdraw,TBDR在这个方面则走得更远一点。所以对于IMR/TBR模式的GPU来说,对不透明物体的draw call从前到后排序、Pre-Z pass都能够显著减少overdraw并提高性能;但对于TBDR模式的GPU来说,这两个策略都不会提升性能(管线里面做了相同的事),而且还会影响因性能(排序、Pre-Z pass带来的额外开销)。

Tips:渲染管线中说的TBDR和我们在引擎的渲染管线中说的TBDR不是一回事,但是这两者又有很大的关系。

关于这三种模式的区别以及演化,强烈建议配合演示动画仔细阅读这篇文章[18]。

什么情景会造成性能瓶颈?

Imagination有两篇[19][20]关于自家PowerVR系列显卡的性能优化建议,其中列举了一些常见的性能优化场景。作为本文的Case Study部分,我会在这两篇的基础上结合前面硬件的原理,去解释其中一些建议的原因。

几何数据优化

减少顶点数量

这个优化简单又直接,减少顶点意味着更少的顶点从System Memory/DRAM里读取到Shader Core(带宽压力),同时意味着更少的VS执行(计算压力),对于Mobile GPU,还意味着更快的Bining Pass(主要是带宽压力)。模型的减面、LOD包括normal map去代替高模体现细节都是同类优化。

减少每个顶点数据量

这个也比较直观,数据量更少意味着VAF阶段和Binning Pass阶段更少的读写开销。甚至有时候,我们可以在VS里使用一些快速的顶点数据压缩/解码方案[21][22](少量的计算开销换取更少的带宽开销)。

避免小三角形

小三角形最直观的缺点就是:在屏幕上占用的像素非常少,是一种视觉上的浪费。实际上,由于硬件管线中,针对三角形有图元装配的环节(Triangle Setup),还有三角形的剔除(Vertex Culling/Triangle Clipping),因此主要是“构造一个三角形的固定开销”。文章[19]中还提到了一定要避免小于32像素的Triangle,我猜是因为小于32像素的三角形在PS阶段,组的Warp可能是不足32pixel的(有待考证)。近几年提出的GPU Driven Pipeline里面,已经有用Compute Shader去剔除小三角形的优化方法[23]。

优化索引缓冲

这是一个很少会有人提到的优化,原理是:VAF的阶段,顶点的数据是根据PD派发的indices patch(长度是几十个顶点索引)从显存里面取的,indices patch相当于把一个长的index buffer切分成小段,在每个indices patch内,同一顶点被访问越多次,memory cache的命中率越高,相应地,带宽开销就越小。所以我们可以通过重排index buffer,让一段indices patch内同一顶点被引用的次数尽可能地多[24]。

Interleaving Attributes vs.Seperate Attributes

通常来说,如果一堆属性在VS中始终是会被一起使用(比如skinned weight和skinned indices;Normal/Tangent),我们应该把它们放在一起以减少Graphics API bind的次数,如果一堆属性在不同VS中使用频率相差很大(比如position非常频繁,但vertex color很少使用),那么我们应该存储在不同buffer。这个原理和AoS/SoA的区别一样,也是尽量提高缓存的利用率(缓存加载的时候的最小单位是Cache Line,通常64/128Bytes,所以要保证每次memory access能load更多有用的数据到cache)。

物件的优化

基于摄像机距离的排序/Z Pre-Pass

这个优化我们之前说过,对于有early-z机制的GPU(IMR/TBR)是有效的,对TBDR无效。

基于材质/RenderState的排序

RenderState是一个比较笼统的称呼,对于OpenGL这类基于状态机的Graphics API,像是buffer/texture绑定,framebuffer切换,shader切换,depth/stencil/culling mode/blend mode等都属于状态切换,并且有性能开销。状态切换中涉及的开销包括driver端的命令验证及生成;GPU内部硬件状态机的重新配置;显存的读写;CPU/GPU之间的同步等。这里有一张图[25]大致量化了各类状态切换的开销:


至于每个graphics API call在调用的背后发生了什么,我没有相关的知识去详细解释。这个主题也足够一篇文章的内容去单独阐述,如果有这方面的资料或者写driver的朋友,希望可以解释一下。

最理想状况当然是把相同材质/RenderState的物体可以合并为一个batch提交,也就是我们常说的减少draw call[26]。

贴图的优化

贴图优化的核心只有一个:Cache Friendly。

减少贴图尺寸

很多人都觉得减少贴图尺寸带来的最大优化是显存,对于主机/移动平台以及一些受显存大小限制的场景或许是对的。但从性能角度分析,减少贴图尺寸带来的最大好处是提高缓存命中率:假设把一张1024*1024的贴图换成一张1*1的贴图,shader不变,你会发现shader的执行速度变快了,因为对所有需要采样这个贴图的shader来说,真正从内存读取数据只需要一次,而后的每次采样,都只需要从cache里取那个像素数据即可。换句话说,我们关注的是每条cache line能够覆盖多少个像素的PS执行。贴图尺寸越小,每条cache line覆盖的被采样像素就越多。

使用压缩贴图

这个思路和顶点压缩是类似的,即牺牲一些计算量用于即时的数据解压缩,来换取更少的带宽消耗。诸如DXT/PVRTC/ASTC都是这样的思路。同样的思路还可以用在紧凑的G-Buffer生成,比如CryEngine曾经用Best Fit Normal和YCbCr色彩空间压缩G-Buffer[27][28]。

合并贴图到Texture Altas

这个其实是为了减少贴图的绑定开销,本质上是减少状态切换。如果有Bindless Texture[25]的情况下,这个优化就帮助不大。

使用Mipmap

通常我们使用Mipmap是为了防止uv变化比较快的地方(一般是远处)的贴图采样出现闪烁,但究其根源,闪烁是因为我们在对相邻像素进行着色的时候,采到的texel是不连续的。这其实就意味着cache miss。而Mipmaped texture会按级存储每一层mip(物理内存上连续),这就意味着当你使用Mipmap去采样的时候,缓存命中率是更高的,因此性能相比没有Mipmap的贴图也会更好。

存储结果到Buffer还是Texture?

有时候我们会把一些通用计算放在GPU上,结果存在buffer/texture上,理论上,如果能够选择的话,尽量把不是图片类型的数据存储在buffer上(比如particle的velocity/pos或者skinPallete,最好用buffer存)。这听起来是一句废话,理由是:Buffer和Texture在内存中的存储布局不一样,Buffer是线性的,Textute是分块的,在非贴图数据的访存模式下,分块的内存布局往往不利于缓存命中。

Tips:当然,对于移动平台来说,Cache Friendly还意味着更少的发热。

Shader的优化

减少分支?

我们已经解释过GPU是如何实现分支的,所以再回到是否要减少分支这件事,就不应该一味地认为分支总是对性能不好的。应该说,如果分支的结果依赖shader在运行时决定,并且这个结果在warp内差异很大,那么我们应该避免分支,实在无法避免时,尽量提取公共计算部分到分支外。近年来大部分的Deferred Shading框架,都依赖于Material ID去判断材质类型,并在shading阶段依赖动态分支去做不同的着色计算,这是因为材质在屏幕空间上的变化是比较少的(大部分使用标准PBR材质),所以分支带来的性能问题也不大。

另外,我们经常会用一些Uber Shader来实现不同的材质效果(但又共享很多公共计算)。实现的思路有两种:用宏定义基于Uber Shader生成不同的Sub Shader和Uber Shader内基于Uniform的分支。前者可能带来的shader切换的开销,后者反倒可能更有利于性能(当然,这个也要具体情况具体分析)。

精确指定数据类型

对于ALU来说,它的许多数学运算指令的时长/并发数是依赖于数据位宽的。因此应该尽量使用算法允许的最低精度数据类型来进行计算,比如GLSL中,可以通过highp/mediump/lowp去指定当前shader的数据计算精度。另外,在进行Int/Float的混合计算时,需要额外的指令对Int类型进行转换,因此,如无必要,尽量不要用Int类数据。

使用向量算法还是标量算法?

过去很多Shader Core的设计是Vector Based,即ALU只会进行向量的加减乘除,对于标量也会规约到向量运算。基于这类设计,就有一些奇技淫巧,去把一个计算尽量向量化[17]。但现在更多的是Scalar Based的Shader Core,所以也就无需太过关注这点,但是,我们还是应该尽量延迟向量和标量之间的运算,比如这个例子:


用贴图缓存中间计算结果?

很多时候,我们会把一些数学上的中间计算结果缓存到一张贴图里,这些贴图的数值本身不代表视觉信息,而是纯粹的数字。比如Marschner Hair Mode用LUT去存BRDF[29];UE4用LUT去存储PBR的环境光BRDF[30]。

LUT带来的性能损耗有两点:

(1)贴图本身是数值,所以只能用无损格式,无法压缩,所以bytes per pixel是比较大的,比一般贴图占用更多读取带宽

(2)对于贴图的采样是基于LUT的uv计算的,而相邻像素算出的uv通常都没有空间连续性,这就表示每次LUT的采样几乎都会导致cache miss,所以这类LUT比一般贴图的采样更慢。

结论:尽量使用拟合函数去代替LUT采样,对于Mobile GPU来说,永远不要尝试用LUT去优化一段shader;对于Desktop GPU来说,慎重考虑使用LUT。

结语

这可能是我写过最累的一篇专栏文章,快写完的时候发现其实这个主题应该拆成三篇来写。优化涉及的内容细碎又繁琐,概念之间彼此相互关联,而且大部分知识,我只会记下出处和大概内容,要写下来的时候,往往还需要重新查找引用并且确认细节。另一方面,这个主题相对来说比较硬核,硬件知识枯燥无味,比起图形算法来说要无趣得多,也没人爱看。以至于写到最后,我都开始怀疑到底为什么要写这个东西。

可能对我来说最大的意义,一是对过往杂乱的知识做一个自我梳理;二是告诉那些面试官,其实我也懂一点优化的(虽然只是嘴炮而已:))。

参考

1.^Life of a Triangle https://developer.nvidia.com/content/life-triangle-nvidias-logical-pipeline
2.^Practical DirectX 12 https://developer.nvidia.com/sites/default/files/akamai/gameworks/blog/GDC16/GDC16_gthomas_adunn_Practical_DX12.pdf
3.^What is a Texture Processor Cluster or TPC?https://www.geeks3d.com/20100318/tips-what-is-a-texture-processor-cluster-or-tpc/
4.^https://en.wikipedia.org/wiki/Branch_predictor
5.^Fermi Hardware&Performance Tips http://theinf2.informatik.uni-jena.de/theinf2_multimedia/Website_downloads/NVIDIA_Fermi_Perf_Jena_2011.pdf
6.^Analyzing GPGPU Pipeline Latency http://lpgpu.org/wp/wp-content/uploads/2013/05/poster_andresch_acaces2014.pdf
7.^FEATURES HARDWARE Nvidia DLSS:An Early Investigation https://www.techspot.com/article/1712-nvidia-dlss/
8.^GTX1080 is Here!https://zhuanlan.zhihu.com/p/20861061
9.^图灵架构特性解析https://zhuanlan.zhihu.com/p/44644238
10.^一篇光线追踪的入门https://zhuanlan.zhihu.com/p/41269520
11.^这是一篇光线追踪的骗赞文https://zhuanlan.zhihu.com/p/51493136
12.^NVIDIA Turing Architecture In-Depth https://devblogs.nvidia.com/nvidia-turing-architecture-in-depth/
13.^https://www.nvidia.com/object/pascal-architecture-whitepaper.html
14.^https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/technologies/turing-architecture/NVIDIA-Turing-Architecture-Whitepaper.pdf
15.^ARM Mali GPU Architecture https://armkeil.blob.core.windows.net/developer/Files/pdf/graphics-and-multimedia/Mali_GPU_Architecture.pdf
16.^PowerVR Hardware Architecture Overview for Developers http://cdn.imgtec.com/sdk-documentation/PowerVR+Hardware.Architecture+Overview+for+Developers.pdf
17.^abGPU architectures https://drive.google.com/file/d/12ahbqGXNfY3V-1Gj5cvne2AH4BFWZHGD/view
18.^abGPU Framebuffer Memory:Understanding Tiling https://developer.samsung.com/game/gpu-framebuffer
19.^abPowerVR Performance Recommendations http://cdn.imgtec.com/sdk-documentation/PowerVR.Performance+Recommendations.pdf
20.^PowerVR Performance Recommendations The Golden Rules http://cdn.imgtec.com/sdk-documentation/PowerVR+Performance+Recommendations.The+Golden+Rules.pdf
21.^完整的顶点压缩http://www.klayge.org/2012/11/11/%E5%AE%8C%E6%95%B4%E7%9A%84%E9%A1%B6%E7%82%B9%E5%8E%8B%E7%BC%A9/
22.^压缩tangent-frame http://www.klayge.org/2012/09/21/%E5%8E%8B%E7%BC%A9tangent-frame/
23.^Optimizing the Graphics Pipeline with Compute https://frostbite-wp-prd.s3.amazonaws.com/wp-content/uploads/2016/03/29204330/GDC_2016_Compute.pdf
24.^OpenGL Insights,30.WebGL Models:End-to-End
25.^abBeyond Porting http://www.ozone3d.net/dl/201401/NVIDIA_OpenGL_beyond_porting.pdf
26.^Batch,Batch,Batch https://www.nvidia.com/docs/IO/8228/BatchBatchBatch.pdf
27.^Rendering Technologies from Crysis 3 https://www.slideshare.net/TiagoAlexSousa/rendering-technologies-from-crysis-3-gdc-2013
28.^CryENGINE 3:Reaching the Speed of Light.
29.^GPU Gems 2,Chapter 23.Hair Animation and Rendering in the Nalu Demo https://developer.nvidia.com/gpugems/GPUGems2/gpugems2_chapter23.html
30.^Real Shading in Unreal Engine 4 https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf

作者:洛城
专栏地址:https://zhuanlan.zhihu.com/p/68158277

相关推荐