95.性能优化-GPU-着色器优化-降低填充率压力-并行特性优化思路
95.1 知识点
善用 GPU 并行特性是什么意思
GPU 的强项是高度并行——成百上千个像素、顶点、线程同时工作。善用并行特性,就是把计算分成许多相对简单、独立、可并行的工作单元,避免破坏并行效率的做法。目标是让每个 GPU 线程做少量且规则的工作,从而提高吞吐量并降低延迟与功耗。
向量化运算
GPU 的 ALU(算术逻辑单元)通常以 4-wide SIMD(单指令多数据流)形式工作,能同时处理 4 个 float 分量。因此在着色器中应尽量使用一次性向量运算。
错误示例:分开计算,浪费 SIMD 宽度。
float r = tex2D(_Tex, uv).r;
float g = tex2D(_Tex, uv).g;
float b = tex2D(_Tex, uv).b;
正确示例:一次性向量运算,GPU 并行处理多个分量。
float3 rgb = tex2D(_Tex, uv).rgb;
// 向量运算示例
float3 a = float3(1, 2, 3);
float3 b = float3(4, 5, 6);
float3 c = a * b; // GPU 并行计算 3 个分量
避免线程发散
GPU 最小的同步执行单元(NVIDIA 的 warp / AMD 的 wavefront)中所有线程必须执行相同指令,分支会导致部分线程闲置。
错误示例:动态分支导致线程发散。
if (dot(N, L) > 0)
color += CalculateLight(N, L);
else
color += float3(0, 0, 0);
正确示例:用 saturate 替代分支,所有线程执行相同指令。
float NdL = saturate(dot(N, L));
color += CalculateLight(N, L) * NdL;
尽量不要在 Shader 中使用 if 语句。如果确实存在分支,建议使用 Shader 关键字生成不同变体。
内存访问合并
连续的全局内存访问可以被 GPU 合并为更少的内存事务;随机访问或间隔很大的索引会导致散列访问,严重拉低带宽效率。
错误示例:随机索引导致散列访问。
groupshared float data[256];
uint index = some_complex_calculation(tid);
float value = data[index];
正确示例:连续索引,GPU 可合并内存事务。
groupshared float data[256];
uint index = tid;
float value = data[index];
预计算并行查找表
先在离线或初始化阶段预计算好复杂函数的结果,运行时通过查表获取,而非实时计算。假设 1024 个像素线程都需要计算 f(x),每个都算一次则性能线性增长;而查找表只需访问一个数组或纹理,GPU 的纹理采样硬件高度并行,访问延迟被隐藏,带宽远比算力充裕,大量线程可同时查表而不互相干扰。
批处理
合并 Draw Call 可减少 CPU 提交压力,让 GPU 在更大的批次中并行工作,提升吞吐。当存在大量相似小物体(草、树、子弹、UI 等)时,应果断使用批处理。
减少与避免昂贵的原子操作
原子操作是不可被中断的操作——要么整个执行完毕,要么根本没开始,保证多个线程对同一变量的读改写不可分割。原子操作一般用在 GPU 并行计算、Compute Shader 或多线程编程中。
但原子操作在高并发下会串行化访问同一内存地址,成为瓶颈。应避免在大并发写入场景(大量线程写同一计数器)直接使用原子操作。
减少数据依赖
数据依赖(Data Dependency)指一个计算必须依赖另一个计算结果才能继续。常见形式:
- 顺序依赖:
float b = a * 2;(必须等a算完)。 - 循环依赖:
data[i] = data[i-1] * 0.5;(每次迭代依赖前一次结果)。 - 前后帧依赖:线程间需要用到其他线程的结果,导致串行化,降低并行度。
应尽量让每个线程独立计算,不依赖其他线程或前序结果:
- 数据拆分:每个线程只操作独立的数据,如
float b = data[id] * 2;。 - 依赖计算移到 CPU:序列依赖逻辑用 CPU 先算,再传给 GPU 并行处理。
- 减少原子操作:避免线程间因共享写入而产生隐式依赖。
95.2 知识点代码
Lesson95_性能优化_GPU_着色器优化_并行特性优化思路.cs
public class Lesson95_性能优化_GPU_着色器优化_并行特性优化思路
{
#region 基本概念 善用GPU并行特性
// 把计算分成简单、独立、可并行的工作单元,
// 避免破坏并行效率的做法,让每个线程做少量规则的工作。
#endregion
#region 知识点一 向量化运算
// GPU ALU 以 4-wide SIMD 工作,应使用向量运算而非分量逐个计算。
#endregion
#region 知识点二 避免线程发散
// warp/wavefront 内所有线程执行相同指令,动态分支导致线程闲置。
// 用 saturate/lerp 替代 if,或用 Shader 关键字生成变体。
#endregion
#region 知识点三 内存访问合并
// 连续内存访问可被合并为更少事务,随机访问严重拉低带宽效率。
#endregion
#region 知识点四 预计算并行查找表
// 离线预计算复杂函数结果存入纹理/数组,运行时查表替代实时计算。
#endregion
#region 知识点五 批处理
// 合并 Draw Call,让 GPU 在更大批次中并行工作。
#endregion
#region 知识点六 减少昂贵的原子操作
// 原子操作在高并发下串行化同一地址访问,避免大量线程写同一计数器。
#endregion
#region 知识点七 减少数据依赖
// 让每个线程独立计算,减少顺序/循环/帧间依赖,
// 依赖逻辑移到 CPU,避免线程间串行化。
#endregion
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com