Shader中的代码优化原理分析

问题概述

在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)和a*b + a*c 以及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)调试器,可以用于真机调试,功能更加强大,但是收费的;

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OpenGL 半球光照原理是一种基于环境光照的光照模型,它使用半球体来模拟环境光照射到物体表面的效果。半球体通常被分成两个部分,一个上半球和一个下半球,上半球表示天空光照,下半球表示地面光照。在这种光照模型,每个顶点都会计算其对应的法向量,然后使用法向量去采样半球体上的纹理,从而得到该点的环境光颜色。 下面是一个简单的OpenGL半球光照的Shader代码: 顶点着色器: ``` #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; out vec3 vPos; out vec3 vNormal; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); vPos = vec3(model * vec4(aPos, 1.0)); vNormal = mat3(transpose(inverse(model))) * aNormal; } ``` 片段着色器: ``` #version 330 core out vec4 FragColor; in vec3 vPos; in vec3 vNormal; uniform vec3 lightColor; uniform vec3 objectColor; uniform vec3 lightPos; uniform vec3 viewPos; uniform samplerCube skybox; void main() { vec3 ambient = 0.2 * lightColor; vec3 norm = normalize(vNormal); vec3 lightDir = normalize(lightPos - vPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor; vec3 viewDir = normalize(viewPos - vPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); vec3 specular = spec * lightColor; vec3 envColor = texture(skybox, normalize(vPos)).rgb; vec3 color = vec3(0.0); color += ambient * envColor; color += diffuse * objectColor * envColor; color += specular * envColor; FragColor = vec4(color, 1.0); } ``` 在这个Shader,我们使用了天空盒纹理来模拟环境光照。通过采样天空盒纹理,我们可以得到当前顶点的环境光颜色。然后我们计算漫反射光和镜面反射光的强度,最终得到该点的最终颜色。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值