unity-shader-GPU优化
unity-shader-GPU优化
前篇
- 主要参考: 优化shader程序的一些tips - https://blog.csdn.net/leonwei/article/details/55519588
首先要树立几个思想:
- gpu是SIMD的架构,即单指令多数据流架构,即在gpu上同时执行n个数据和执行1个数据的效率是一样的,我们要
尽量的把并行的计算搬到gpu上
-  gpu是以向量计算为基础设计的,也就是说在gpu上执行一个向量乘法和执行一个float的乘法的效率是一样的,
并不向cpu那样要多执行几次
技巧
- 尽量把一些计算合并成向量计算,记住一个向量计算和一个float计算那样快!比如 - 1 
 2
 3- float x,y; 
 x = x * a;
 y = y * b;- 不如写成 - 1 
 2
 3- // 
 float2 v = float2(x,y);
 v = v*float2(a,b);- 因为前一种写法是两次乘法计算,而后一种只要1次 
- 慎用 分支与循环. 尽管大多数gpu 的shader支持这种语法,但是多数gpu里面的 - 这种控制语句涉及到一些同步等消耗的操作,其实大多数这种语句都可以用数值的方式替代。比如 - 1 
 2
 3
 4
 5
 6- float4 a; 
 if(b < 1) {
 a.a=1;
 } else {
 a.a =0.5;
 }- 可以改写成 - 1 
 2
 3
 4- float4 a; 
 float tmp = step(b,1);
 a = lerp(1, 0.5, tmp); // unity 中
 // a = mix(1, 0.5, tmp); // glsl 中- 而这种操作,尤其是shader内置的函数比条件判断和分支的效率要高很多,别忘了,GPU纯粹是为了计算的,而不是做判断. - 它们在GPU的实现和CPU有很大不同,在最坏的情况下,我们花在一个分支语句的时间相当于运算了所有分支语句的时间。所以我们不鼓励使用流程控制语句,它们会降低GPU的并行处理操作。一个解决方案是我们尽量把计算向流水线上端移动,例如把片元着色器的运算放在顶点着色器,或在CPU预计算,把结果传给Shader。如果非要用它们,建议是: - 分支判断语句中使用的条件变量最好是 常数,不会发生变化。 - 也就是静态分支, 参考: Shader中的条件分支能否节省shader的性能?- https://www.zhihu.com/question/329084698/answer/714757220 
- 每个分支中包含的操作指令尽可能少。 
- 分支的嵌套层数尽可能少。 
 
- 不要除以 0 . 不然结果不可预测。 
- 尽量使用shader为我们提供的内置函数,这些内置的函数比我们想象的要快很多,往往应用了某些gpu的特殊 - 特性。 - 比如要比较a和b谁大用 - max(a,b),还有例如上面反复用的step,虽然你可以写用(float)(a>=1)来替换- step(1,a)- 但是这还是没有内置函数更快的,包括常用的 - saturate()把一个数归到0-1,总之一句话,如果能用一个内置函数替换- 你的某些代码,就尽量替换。而且这些内置函数基本上都是支持对向量操作的,所以如果用 - step(a, fixed3(1, 2, 3))其- 实只是一条指令,但是却可以同时返回用a同1 2 3分别比较的结果。 
- 使用swizzle是非常快的,例 - float4 a = float4(1,1,1,1),用- a.wz = float2(2,3)要比- a.w=3; a.z=2要高效很多
- 使用合适的数据类型,大部分gpu支持 float 的数值类型基本上分为 fixed half float,分别是12位的定点数,16位的浮 - 点数 以及 32位的浮点数,尽可能的选择位数更少的数据类型来加快操作 
- 减少从 cpu 到 gpu 的传输量. - 比如原来传 3 个 float 变量, 可以改成 把 3 个 float 丢到 vector4 里 传到 gpu 中, 然后再通过 a.x, a.y, a.z 取出来使用. - 同样贴图 sample 也是, 原来传 一个 albedo 和 一个 ao, 如果没有 alpha 的话, 可以把 ao 合到 albedo 的 a 通道中, 只传一个 贴图, 然后 tex.a 取出来使用 
分支 效率问题
参考: shader中用for,if等条件语句为什么会使得帧率降低很多?- https://www.zhihu.com/question/27084107
这个问题分为三种情况 完全静态 , 动静态 和 完全动态
- 完全静态 - 编译期 就可以确定的条件, 比如 - 两个常量进行比较- 1 
 2
 3
 4
 5
 6
 7- float a = 0.2; 
 float b = 0.3;
 if (a < b) {
 col *= fixed4(1, 0, 0, 1);
 } else {
 col *= fixed4(0, 1, 0, 1);
 }- 此时编译器可以直接摊平分支,或者展开(unloop)。对于For来说,会有个权衡,如果For的次数特别多,或者body内的代码特别长,可能就不展开了,因为会指令装载也是有限或者有耗费的
- 额外成本可以忽略不计
 
- 动静态 - 运行期 才可以确定的条件, 比如 - 常量 与 uniform变量进行比较- 1 
 2
 3
 4
 5
 6
 7
 8
 9- _Height ("Height", float) = 0.5 
 float _Height;
 float a = 0.2;
 if (a < _Height) {
 col *= fixed4(1, 0, 0, 1);
 } else {
 col *= fixed4(0, 1, 0, 1);
 }- 一个运行期固定的跳转语句,可预测 
- 同一个Warp内所有micro thread均执行相同分支 
- 额外成本很低 
- 汇编代码 - 左边是 完全静态 分支, 右边是 动静态 分支 ( 使用的是 RenderDoc )  - 可以看出, 动态分支 会把所有的分支代码都跑一边, 而 静态分支 只跑符合 true 条件的分支. 
 
- 完全动态 - 运行期 才可以确定的条件, 比如 - uniform变量 与 uniform变量进行比较- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- _Height ("Height", float) = 0.5 
 _Height2 ("Height", float) = 0.6
 float _Height;
 float _Height2;
 if (_Height < _Height2) {
 col *= fixed4(1, 0, 0, 1);
 } else {
 col *= fixed4(0, 1, 0, 1);
 }- 这才是真正的“动态分支”
- 会存在一个Warp的Micro Thread之间各自需要走不同分支的问题
 
精度问题
- float: high precision floating point. Generally 32 bits, just like float type in regular programming languages.
- half: medium precision floating point. Generally 16 bits, with a range of –60000 to +60000 and 3.3 decimal digits of precision.
- fixed: low precision fixed point. Generally 11 bits, with a range of –2.0 to +2.0 and 1/256th precision.
定位瓶颈的办法:
相关参考
- Shader 优化相关资料整理 - https://blog.csdn.net/panda1234lee/article/details/54861041
- 改变帧缓冲或者渲染目标(Render Target)的颜色深度(16 到 32 位), 如果帧速改变了, 那么瓶颈应该在帧缓冲(RenderTarget)的填充率上。 
- 否则试试改变贴图大小和贴图过滤设置, 如果帧速变了,那么瓶颈应该是在贴图这里。 
- 否则改变分辨率.如果帧速改变了, 那么改变一下pixel shader的指令数量, 如果帧速变了, 那么瓶颈应该就是pixel shader. 否则瓶颈就在光栅化过程中。 
- 否则, 改变顶点格式的大小, 如果帧速改变了, 那么瓶颈应该在显卡带宽上。 
- 如果以上都不是, 那么瓶颈就在CPU这一边。