1 精度问题
精度越低的数值类型意味着更低的寄存器数量,更快的计算,以及更低的能耗。
能耗:half < float < short < int. 注意,最大的是int!
不过需要注意的是half的取值范围,IEEE754规定binary16类型用1个符号位,5个指数位,10个有效位。所以
1 2 |
0 00001 0000000000 = 2−14 ≈ 6.10352 × 10−5 (最小值) 0 11110 1111111111 = 65504 (最大值) |
嗯,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
