1 精度问题

精度越低的数值类型意味着更低的寄存器数量,更快的计算,以及更低的能耗。

能耗:half < float < short < int. 注意,最大的是int!

不过需要注意的是half的取值范围,IEEE754规定binary16类型用1个符号位,5个指数位,10个有效位。所以

嗯,65536就是INF了.

以及误差需要注意。在1附近,误差是2^-10,大约0.001。在4096到8192的范围,误差是4。

写shader时可以instanceid,vertexid,threadid都用ushort(不过shaderlab没有ushort是为啥。。。),写字面量时值的后面加个h就表示half了。

2. 算术问题-乘加MAD

shader一般会有一个硬件指令MAD,一次计算一个乘加,如Metal的fma(a,b,c) = a * b + c。这样比乘一次加一次少一个指令。

因此很多一次函数形式的变换都可以用乘加的形式重新表述,下面给了很好的例子

甚至除法也可以用MAD来优化

 

3. 某些情况下免费的abs,neg,saturate

用abs和neg来处理一个输入变量基本是免费的,但是如果处理的不是输入变量而是运算结果,其实不免费

用saturate处理输出变量是免费的,但如果处理的是中间变量而不是输出结果,就不免费

4. 直接使用硬件指令,并利用中间结果

比如pow这个操作,其实调用的log2和exp2,log会调用log2,exp会调用exp2。因此用log和exp的时候可以预计算一个系数直接用log2和exp2

像z * pow (x ,y)的操作,预计算log2(z)就能省一次乘法

下面这些就不要乱用了

length()其实计算的sqrt(dot()),normalize计算的 *= rsqrt(dot()),共用的rsqrt(dot())可以提出来只算一次

w为1的向量的矩阵变换,可以自己展开剩一个运算。。。。

5. 区分scalar和vector

尽管gpu是SIMD的,但对标量计算也是有优化的,因此计算时候可以分开两种。

比如下面这个,一个连乘,如果区分标量矢量的话可以省一步指令

 

6. 分支问题

大家都知道shader写ifelse比较慢,主要原因在于gpu是流水线作业,同一个warp会等所有thread的操作分支结束在回合再继续。所以尽量避免分支。

避免分支可以用三元运算符?:,据说Apple A8以后硬件级别优化了这个操作符。不知道实现方法是不是bitwiseselect按位选择,下面是一个SSE2的bitselect,在shader里面写法基本一样。

如果一定要分支的话,注意下面这个对比,注意贴图读取是很慢的,可能在十个指令数左右(VTF是20个指令数?),前两个需要等待两次读取,后面的等一次就行了,这样减少了了流水线的延迟

参考资料:

GPGPU Programming for Games and Science

GDC2013  Low-level Thinking in High-level Shading languages

WWDC16 Session 606 Advanced Metal Shader Optimization

IEEE754 – half precision

 

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注