问题概述
在shader代码的编写中不能只依赖于编译器的优化,它的优化很有局限性,编译器只能去理解shader的语义,他可以移除无用的代码及资源,但并不会把一个add+mul代码优化成mad指令;
重构源代码以减少所需的计算数量,而不是依赖编译器来应用优化。
为了优化shader代码,我们需要知道代码从被编写到被执行的流程,知道什么样的代码是不好的;
一些通用的shader代码规范
- 避免if、switch分支语句
- 避免for循环语句,特别是循环次数可变的
- 减少纹理采样次数
- 减少复杂数学函数调用
- 降低浮点数精度
从汇编层来看代码的效率
swizzle语法
由于硬件设计,执行一维数据和四维是一样的时间,shader提供了几个向量组件rgba,stpq,xyzw;我们需要充分利用这种硬件特性,提高硬件效率,避免算力的浪费;
如下是一组测试,分别做了一个float4的乘法和一个float的乘法,观察汇编指令,发现指令数并没有增加。
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float4 ret = col.rrrr * col.bbbb;
return fixed4(ret.xyz, 1);
}
// 汇编
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: mul o0.xyz, r0.zzzz, r0.xxxx
2: mov o0.w, l(1.000000)
3: ret
// 计算单个数据
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float ret = col.r * col.b;
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: mul o0.x, r0.z, r0.x
2: mov o0.yzw, l(0,0,0,1.000000)
3: ret
通过dot点积代替add,如下,如果使用加法将四个分量相加,则需要三个add指令,如果使用内置点乘,则需要一个dp4指令。
Vectorization:向量化思维,Scalar Code和Vectorized Code;拆分标量的数据,放入到向量中,但是要注意不要产生额外的数学计算,否则就可能是个负优化;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed ret = col.r + col.g + col.b + col.a;
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: add r0.x, r0.y, r0.x
2: add r0.x, r0.z, r0.x
3: add o0.x, r0.w, r0.x
4: mov o0.yzw, l(0,0,0,1.000000)
5: ret
//
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 arg4 = 1.0;
fixed ret = dot(col, arg4);
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: dp4 o0.x, r0.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)
2: mov o0.yzw, l(0,0,0,1.000000)
3: ret
mad指令和rcp指令
mad是mul add的缩写,我们需要在代码中使用mul add的写法,因为这种写法会将计算使用一条mad指令来完成,否则可能就需要使用add和mul两条指令来完成;
如下是一个测试,测试了a(b+c)和ab + ac 以及a(1+c)和a + a*c所需要的指令;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float ret = col.r * (0.1 + col.g);
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: add r0.y, r0.y, l(0.100000)
2: mul o0.x, r0.y, r0.x
3: mov o0.yzw, l(0,0,0,1.000000)
4: ret
//
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float ret = col.r * 0.1 + col.r * col.g;
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: mul r0.y, r0.y, r0.x
2: mad o0.x, r0.x, l(0.100000), r0.y
3: mov o0.yzw, l(0,0,0,1.000000)
4: ret
//
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float ret = col.r + col.r * col.g;
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: mad o0.x, r0.x, r0.y, r0.x
2: mov o0.yzw, l(0,0,0,1.000000)
3: ret
mad相关的一些常用写法
主要要有1.0/b,不要直接写成除法,那么就会生成div指令,而不是mad指令了;
rcp,将除法转换为乘以除数的倒数,
插值计算,如下第一种方式是最常见的写法,但是它最终需要三个指令来完成,而第二种写法则需要两个指令就可以完成;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float ret = col.r * col.a + col.g * (1 - col.a);
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: add r0.z, -r0.w, l(1.000000)
2: mul r0.y, r0.z, r0.y
3: mad o0.x, r0.x, r0.w, r0.y
4: mov o0.yzw, l(0,0,0,1.000000)
5: ret
///
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float ret = (col.r - col.g) * col.a + col.g;
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: add r0.x, -r0.y, r0.x
2: mad o0.x, r0.x, r0.w, r0.y
3: mov o0.yzw, l(0,0,0,1.000000)
4: ret
swizzle+mad:通过mad以及swizzle来代替mov
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 ret4;
ret4.rgb = col.rgb;
ret4.a= 1.0;
return ret4;
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: mov o0.xyz, r0.xyzx
2: mov o0.w, l(1.000000)
3: ret
///
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 ret4;
fixed2 ret2 = float2(1, 0);
ret4.rgb = col.rgba * ret2.xxxy + ret2.yyyx;
return ret4;
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: mov o0.xyz, r0.xyzx
2: ret
尽量使用内置函数
GPU硬件支持,SFU(Special function units),SIMD架构的支持,提高ALU的利用率,if-else分支的最差情况可能是有1/n的利用率。使用统一的控制流,不要使用分支。
总结:
- 避免过度地规范化
- abs,negate,saturate是免费的,不占用指令槽位,saturate是免费的,但是min和max不是,所以要优先使用saturate(将变量限制到01区间);
- 避免sign的使用
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed ret = abs(col.r) + abs(col.g);
return fixed4(ret, 0, 0, 1);
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: add o0.x, |r0.y|, |r0.x|
2: mov o0.yzw, l(0,0,0,1.000000)
3: ret
注意以下写法的区别:第一个frag,两种计算方式,第二种会多一个负数的指令;第二个frag,使用sign,会使用更多的指令;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed ret = -col.r * col.g;
// fixed ret = -(col.r * col.g);
return fixed4(ret, 0, 0, 1);
}
//
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
// fixed ret = sign(col.r) * col.g;
fixed ret = col.r > 0 ? col.g : -col.g;
return fixed4(ret, 0, 0, 1);
}
减少分支的使用
ALU Utilization,ALU的利用率;
if-else分支的最差情况可能是有1/n的利用率。
尽量使用统一的控制流,不要使用分支。
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
if (col.a > 0.5){
col.r = 1;
}
return col;
}
/
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.r = col.a > 0.5 ? col.r : 1;
return col;
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: lt r1.x, l(0.500000), r0.w
2: movc o0.x, r1.x, r0.x, l(1.000000)
3: mov o0.yzw, r0.yyzw
4: ret
//
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.r = lerp(col.r, 1, step(col.r, 0.5));
return col;
}
0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
1: ge r1.x, l(0.500000), r0.x
2: and r1.x, r1.x, l(0x3f800000)
3: add r1.y, -r0.x, l(1.000000)
4: mad o0.x, r1.x, r1.y, r0.x
5: mov o0.yzw, r0.yyzw
6: ret
lt指令,less than;conditonal move(cmov指令),提高管线效率;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
if (col.a > 0.5){
col.r = 1;
col.g = (col.r + col.g) / 2;
col.b = (col.r + col.b) / 2;
}
return col;
}
0: mov r0.x, l(1.000000)
1: sample r1.xyzw, v0.xyxx, t0.xyzw, s0
2: add r2.xy, r1.yzyy, l(1.000000, 1.000000, 0.000000, 0.000000)
3: mul r0.yz, r2.xxyx, l(0.000000, 0.500000, 0.500000, 0.000000)
4: lt r0.w, l(0.500000), r1.w
5: movc o0.xyz, r0.wwww, r0.xyzx, r1.xyzx
6: mov o0.w, r1.w
7: ret
注意:现在编译器更加聪明,会自动做一些swizzle技巧的操作;
不要有太多假设,一切以编出来的汇编代码和profile工具为基准。
推荐阅读
https://thegamedev.guru/category/unity-gpu-performance/,gpu优化的一个小网站,一些关于gpu优化的文章合集;
https://zhuanlan.zhihu.com/p/122467342,对shader中if-else分支的理解;
https://developer.nvidia.com/gpugems/gpugems2/part-iv-general-purpose-computation-gpus-primer/chapter-35-gpu-program-optimization,gpu gems shader编程优化,部分思路有些过时(软硬件的发展);
ALU Utilization,ALU的利用率,耗电量;
注意:现在编译器更加聪明,会自动做一些swizzle技巧的操作;注意:现在编译器更加聪明,会自动做一些swizzle技巧的操作;
简而言之,不要有太多假设,一切以编出来的汇编代码和profile工具为基准。
一些用于优化的属性
HLSL中用于优化的指令,[branch],[flatten],[loop],[unroll]
- branch,shader会根据判断语句只执行当前情况的代码
- flatten,shader会执行全部情况的分支代码,然后再根据判断条件获得结果
- unroll,for循环是展开的,直到循环条件终止
- loop,for循环不展开,
if语句默认属性为flatten,为了不破坏并行性;for语句默认是loop;
#if defined(UNITY_COMPILER_HLSL)
#define UNITY_BRANCH [branch]
#define UNITY_FLATTEN [flatten]
#define UNITY_UNROLL [unroll]
#define UNITY_LOOP [loop]
#define UNITY_FASTOPT [fastopt]
#else
#define UNITY_BRANCH
#define UNITY_FLATTEN
#define UNITY_UNROLL
#define UNITY_LOOP
#define UNITY_FASTOPT
#endif
推荐阅读
https://thegamedev.guru/category/unity-gpu-performance/,gpu优化的一个小网站,一些关于gpu优化的文章合集;
https://zhuanlan.zhihu.com/p/122467342,对shader中if-else分支的理解;
https://developer.nvidia.com/gpugems/gpugems2/part-iv-general-purpose-computation-gpus-primer/chapter-35-gpu-program-optimization,gpu gems shader编程优化,部分思路有些过时(软硬件的发展);
深入GPU硬件架构及运行机制,非常全面的关于GPU架构和编程的总结;
二、分析工具(Arm Mali Offline Compiler)
难以像CPU代码一样统计一个函数的耗时,因为它采用的是SIMT架构,也就是单指令多线程处理,多个线程运行同一份shader代码,它们的代码可能会有不同的执行路径。
If you can't measure, you can't improve!
Mali Offline Compiler从以下几个方面来分析:Texture,Load/Stor,arithmetic,Varying,从而进一步找到性能瓶颈;
参考如下静态分析报告:
GLSL 三种变量类型(uniform,attribute和varying)理解
Unity中的shader分析:VS和PS;
- 将shader编译成GLSL代码,需要选择GLES,如果用DX编译则生成汇编代码;
- 将vert和frag分别放到两个文件中,分别以.vert和.frag为后缀;
数据分析指标解析:
- 关于时钟周期的测量完全基于程序中指令的执行成本,因此它不能完全代表实际性能;实际性能你还取决于指令序列中未知的输入,如纹理采样器配置,纹理格式,比如使用三线性过滤将会使纹理循环计数加倍;
- 最短和最长控制流测量是基于着色器源代码中可能的结果,因此,它只是确定了性能的范围区间,但是对于着色器的实际使用并不准确;
官方使用文档:https://developer.arm.com/documentation/101863/0701/(arm官方开发网站包含很多GPU开发和优化的内容)
https://thegamedev.guru/unity-gpu-performance/shader-cost-analysis-mali-offline-compiler/,使用mali offline compiler分析unity shader;
mali还提供了mali graphics debugger(MGD)调试器,可以用于真机调试,功能更加强大,但是收费的;