在学完并实现路径追踪之后,即使增加了多线程渲染,在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的次数,而减少Sampler可以提高性能的原因我当时并没有找到。
这就引出了第二和第三个问题,Sampler的数量为什么会影响性能?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指令。
每个Thread都有自己的寄存器, 这些寄存器都从register file进行分配,如果shader使用过多的寄存器,就会导致更少的Warp Scheduler和更少的Threads, 而更少的Warp Scheduler则意味着GPU的Core可能跑不满(类比操作系统,如果所有Thread都Sleep, 那CPU就在空转是一样的), GPU的性能就得不到发挥。
而根据Wiki的解释 和 另一篇文章, TMU( texture mapping units)和Shader中的sampler是一致的,当我们调用Sampler去采样纹理时,本质上就是调用某一个TMU去采样纹理。
TMU是一种硬件资源,当shader使用过多的Sampler时,会造成同时进行纹理采样的Thread变少,会变相阻碍Thread/Warp的并行度。
纹理采样时,会首先向Texture Cache中去读取,如果读到不到就会从L2加载到Textuer Cache, 如果L2也没有就会从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之前,我们地形的每一层图片尺寸都是1024*1024的图片,并且被渲染出的像素尺寸只有256*256大小, 这样在渲染相邻的pixel时被采样的texel在内存中是不连续的(会跳4个像素), 因此在纹理采样过程中会频发触发Texture Cache Miss, 每次Cache Miss都需要额外的周期从L2或DRAM中重新加载。
使用了Mipmapping之后,GPU可以根据当前的渲染情况来判断采用哪一个Mip Level。
当选择合适的Mip Level之后, 相邻的pixel对应的texel也会尽可能的相邻,可以极大的缓解Texture Cache Miss的状况。
2. Sampler的数量为什么会影响性能?
根据上文分析,如果使用Sampler过多,就会导致每一个Thread占用过多的TMU资源,会影响Thread的并行度
3. 为什么bindtexture开销比较大?
暂时未找到合理的解释
在查资料过程中,有两个额外的收获:
bindtexture并不是用来从CPU向GPU上传图片,在opengl中上传图片是使用glTexImage2D来实现的,这时图片只在显存中。
在fragment阶段,并不是每一个像素都被任意分配到一个Thread然后并行执行的。
一个Warp Scheduler被分成8*4个线程组,每2x2的像素块,被分配给一个数量为4的Thread组, 也是就说每2x2的像素块一定被分配给在同一个Warp Scheduler中的4个Thread。具体原因英伟达的文章上并没有细说。但是大概意思是,比如在决定mip level时,除非这4个像素uv跳跃太大,不然可以只用计算一次mip level就可以了。
https://blog.gotocoding.com/archives/1453