更新一些GPU相关知识

学完并实现路径追踪之后,即使增加了多线程渲染,在SPP=1024的情况下,依然需要30+分钟才能渲染一帧。

为了更快的渲染速度 ,我试图通过使用GPU的CUDA SDK来加速渲染。然而测下来竟然还没我的CPU跑的快,一方面我没有更好的显卡,另一方面我也不太确定是不是我CUDA使用错误所致。再加上就算使用GPU也不可能达到每帧秒级渲染。于是GPU的学习就搁置了。

然而在最近研究Splat地形渲染方案时, 我无意间发现了一个现象。测试地形为4层混合,在所有Texture都不开Mipmapping的情况下, FPS只有30左右,而开了Mipmapping之后,FPS可以稳定在60. 这激起了我强烈的好奇心,终于将了解GPU的运行架构提上日程(又产生了一次PageFault, 本来我在学习《mysql是怎么运行的》这本书,都已经快把B+ Tree看完了) 。

对照《GPU 精粹1》中的【28.2节 定位瓶颈】得出一个结论,如果Texture Filtering会影响FPS, 那么就说明瓶颈在Texture Bandwidth。

这引出我的第一个问题,Texture Bandwidth到底是什么,为什么Mipmapping会影响Texture Bandwidth?

我最开始以为是从CPU到GPU之间传输图片的带宽,越查资料越确定不是这样。

在找到影响FPS的因素之前,其实我大约花了一天试验了各种设置(这就是基本功不够扎实的现象,没有头绪各种试),甚至在FPS达到60时,我都搞不清到底是改了哪个设置变好的。

当然在这期间我也查了很多资料,其中最重要的两个点是说,对于Splat地形方案,他们都会提到减少Sampler的个数,并且提到使用 TextureArray可以改善性能。使用TextureArray可以改善性能的原因是因为它减少了bindtexture的次数,而为什么要减少SamplerState我当时并没有找到依据。

这就引出了第二和第三个问题,为什么要减少SamplerState的数量,是不是性能问题?bindtexture为什么会很“贵”?

直到昨天我发现了一篇NVIDIA讲解GPU架构的文章, 这篇文章虽然不长,但是指出了各种我们在写shader时需要知道的要点。


我先简要概述这篇文章,然后试图来解释这三个问题。

在GPU中有Warp Scheduler, thread, register file, TMU, TextureCache等概念。

Warp Scheduler是最基本的调度单元,也就是说整个Warp Scheduler中的thread一直在执行“齐步走”逻辑. 如果有一个Thread需要换出(switch out)比如等待内存加载), 整个Warp Scheduler的所有Thread都会换出(switch out)。

只要有一个Thread 的if () 判断为真,那所有的Thread都需要执行if为真的逻辑,即使有的Thread的if判断为假,也需要等待if为真的Thread都执行完才执行else, 而之前那部分if为真的Thread同样需要等待else的语句执行完再继续“齐步走”。

每个Warp Scheduler会有32个Thread。这么理解下来其实每个Warp Scheduler就相当于一个具有32通道的SIMD指令(英伟达把他叫做SIMT)。

每个Thread都有自己的寄存器, 这些寄存器都从register file进行分配,如果shader使用过多的寄存器,就会导致更少的Warp Scheduler和更少的Threads, 而更少的Warp Scheduler则意味着GPU的Core可能跑不满(类比操作系统,如果所有Thread都Sleep, 那CPU就在空转是一样的), GPU的性能就得不到发挥。

根据Wiki的解释,纹理采样主要是通过TMU模块进行执行的,TMU模块是一种有限的硬件资源, 因此你采样更多(不一样的)纹理,就需要消耗更多的周期。

纹理采样除了计算之外,还需要加载纹理数据,TMU会首先向Texture Cache中去加载,如果Cache Miss就会从L2加载到Textuer Cache, 如果L2也Cache Miss,就会从DRAM(显存)中加载纹理,然后依次填充L2和Texture Cache.

根据英伟达说明的GF100内存架构从Thread读到Texture Cache只需要几十个周期,而从L2向DRAM加载则需要几百个周期。在这些周期内,需要采样纹理的Warp Scheduler都需要被换出(swap out)。

至目前为止,其实已经能解释前两个问题了:

  1. Texture Bandwidth到底是什么,为什么Mipmapping会影响Texture Bandwidth?

    Texture Bandwidth其实就是指Texture 从DRAM到L2和L2到Texture Cache的加载带宽

    没有使用Mipmapping之前,我们地形的每一层图片尺寸都是10241024的图片,并且被渲染出的像素尺寸只有256256大小, 这样在渲染相邻的pixel时被采样的texel在内存中是不连续的, 因此在纹理采样过程中会频发触发Texture Cache Miss, 每次Cache Miss都需要额外的周期从L2或DRAM中重新加载。

    使用了Mipmapping之后,GPU可以根据当前的渲染情况来判断采用哪一个Mip Level。当选择合适的Mip Level之后, 相邻的pixel对应的texel也会尽可能的相邻,可以极大的缓解Texture Cache Miss的状况。

  2. 为什么要减少SamplerState的数量,是不是性能问题?

    根据微软文档显示,Direct3D 11中SamplerState最大上限为16,但是采样纹理数最大上限为128。可能是因为别人使用Splat方案时,地形层数远超16层。当然也有可能是性能问题,但是我没查到具体依据。

    根据英伟达的说明,具体执行采样是由TMU来执行的,而根据SIMT的特性,同一时间只有一个纹理会被采样,所以理论上SamplerState的多少并不会影响TMU的执行和并发度。

    我反汇编了shader(D3D版本), 看到一个现象,每当我定义一个SamplerState, 就会有一行dcl_sampler的语句,我查了一下MSDN, 发现这个语句是用来声明sampler寄存器。所以如果非要说减少SamplerState可以提高性能,那原因应该就是,使用更多的sampler寄存器,可以获得更多的Warp来增加GPU的并行度。

  3. 为什么bindtexture开销比较大?
    暂时未找到合理的解释


在查资料过程中,有两个额外的收获:

bindtexture并不是从CPU向GPU上传图片,在opengl中上传图片是使用glTexImage2D来实现的,这时图片只在显存中。

在fragment阶段,并不是每一个像素都被任意分配到一个Thread然后并行执行的。

一个Warp Scheduler被分成8*4个线程组,每2×2的像素块,被分配给一个数量为4的Thread组, 也是就说每2×2的像素块一定被分配给在同一个Warp Scheduler中的4个Thread。具体原因英伟达的文章上并没有细说。但是大概意思是,比如在决定mip level时,除非这4个像素uv跳跃太大,不然可以只用计算一次mip level就可以了。



发表评论