100.性能优化-GPU-着色器优化-ComputeShader-组内线程数量
100.1 知识点
Compute Shader 中的执行模型
Compute Shader 的执行模型可分为三层结构:
第一层 —— 线程
- HLSL 函数内部自动生成
- 由 GPU 并行执行的最小计算单元
- 负责执行入口函数的一次调用
第二层 —— 线程组
- 在 Compute Shader 中由
[numthreads(x, y, z)]定义 - 每个组内包含固定数量的线程
- 共享组内内存、可进行同步
- 在 Compute Shader 中由
第三层 —— 线程组网格
- C# 侧
Dispatch()调用 - 定义多少个组要被 GPU 启动
- 控制全局总线程数
- C# 侧
相当于:
- GPU 线程会执行入口函数
- 入口函数前的
[numthreads(x, y, z)]决定单个线程组内的线程数量 - C# 侧调用
Dispatch时传入的 x、y、z 决定要启动多少个线程组
三个轴向的数字代表什么
[numthreads(x, y, z)] 用于控制每个线程组一共有多少个线程。比如:
[numthreads(8, 8, 1)]
表示每个线程组有 8 × 8 × 1 = 64 个线程。
C# 侧调用 Dispatch 时传入的 x、y、z 决定一共有多少个线程组。比如:
[numthreads(8, 8, 1)]
Dispatch(kernel, 64, 64, 1);
表示一共会开启 (64×8, 64×8, 1×1) = (512, 512, 1) = 512 × 512 × 1 = 262144 个线程。
也就是说它们共同决定了线程总数:
总线程数 = numthreads(x, y, z) × Dispatch(groupsX, groupsY, groupsZ)
= (groupsX × x, groupsY × y, groupsZ × z)
= groupsX × x × groupsY × y × groupsZ × z
注意:
- 每个轴的线程或线程组数不能设置为 0,最小为 1
- 1D 任务(一维数据,比如处理线性数组计算):
x, 1, 1 - 2D 任务(二维数据,比如处理图像、纹理等数据):
x, y, 1 - 3D 任务(三维数据,比如处理体积型数据,3D 纹理、网格等数据):
x, y, z - x, y, z >= 1
数值规则
[numthreads(x, y, z)] 的规则
三个参数都是常量整数,取值范围:
- 每个维度 >= 1
- 每个维度 <= 1024
- x × y × z <= 1024
常见组合:(8,8,1)、(16,16,1)、(32,1,1)
Dispatch(groupsX, groupsY, groupsZ) 的规则
三个参数都是整数,取值范围:
- 每个维度 >= 1,不能为 0
- 没有硬性上限(取决于 GPU 支持的最大 Dispatch 数量),但通常非常大(几十万没问题)
匹配规则
当要处理二维纹理、数组、体积数据时,应该让线程数覆盖整个数据区域。一般情况下,线程数和分组数的公式如下:
int groupsX = Mathf.CeilToInt(width / (float)numThreadX);
int groupsY = Mathf.CeilToInt(height / (float)numThreadY);
int groupsZ = Mathf.CeilToInt(depth / (float)numThreadZ);
groups:Dispatch传入的值numThread:numthreads传入的值width、height、depth:代表你要处理的数据维度数量
CeilToInt 向上取整的目的是为了即使不是整数倍也能完全覆盖,超出部分的线程可以在 HLSL 中用边界判断过滤掉。
举例说明
比如要利用 Compute Shader 处理一个 512×512 像素的二维图像,numthreads 为 [numthreads(8, 8, 1)],那么在 C# 侧:
int groupsX = Mathf.CeilToInt(512f / 8); // = 64
int groupsY = Mathf.CeilToInt(512f / 8); // = 64
Dispatch(kernel, groupsX, groupsY, 1);
// 实际启动的线程为 (64×8, 64×8, 1×1) = 512 × 512 = 262144
// 每一个线程处理一个像素,利用 GPU 并行性提升了效率
推荐规则
| 数据维度 | 推荐 numthreads | 说明 |
|---|---|---|
| 1D 数据 | (64,1,1) 或 (128,1,1) |
线性数组、粒子等 |
| 2D 数据 | (8,8,1) 或 (16,16,1) |
图像、纹理等 |
| 3D 数据 | (4,4,4) 或 (8,8,8) |
体积数据、3D 纹理等 |
主要原因:GPU 线程调度单位是 Warp(NVIDIA) / Wavefront(AMD)(32 或 64 个线程),所以最好让 x × y × z 是 32 或 64 的倍数。
总结
numthreads定义线程组的大小(最多 1024 个线程)Dispatch定义要开启多少个组,一般按 数据尺寸 / numthreads 向上取整计算- 总线程数 = 它们的乘积
- 所有维度都必须 >= 1
- 1D:处理线性数据(数组、粒子)
- 2D:处理平面数据(图像、屏幕)
- 3D:处理体积数据(3D 纹理、网格)
100.2 知识点代码
Lesson100_性能优化_GPU_着色器优化_ComputeShader_组内线程数量.cs
using UnityEngine;
public class Lesson100_性能优化_GPU_着色器优化_ComputeShader_组内线程数量 : MonoBehaviour
{
void Start()
{
#region 知识点一 ComputeShader 中的执行模型
// Compute Shader 的执行模型可分为三层结构
// 第一层 —— 线程
// HLSL 函数内部自动生成
// 由 GPU 并行执行的最小计算单元
// 负责执行入口函数的一次调用
// 第二层 —— 线程组
// ComputeShader 中
// 由 [numthreads(x, y, z)] 定义
// 每个组内包含固定数量的线程
// 共享组内内存、可进行同步
// 第三层 —— 线程组网格
// C# 侧 Dispatch() 调用
// 定义多少个组要被 GPU 启动
// 控制全局总线程数
// 相当于
// 1. GPU 线程会执行入口函数
// 2. 入口函数前的 [numthreads(x, y, z)] 决定单个线程组内的线程数量
// 3. C# 侧调用 Dispatch 时传入的 x、y、z 决定要启动多少个线程组
#endregion
#region 知识点二 三个轴向的数字代表什么
// 1. 入口函数前的 [numthreads(x, y, z)] 用于控制每个线程组一共有多少个线程
// 比如:
// [numthreads(8, 8, 1)]
// 表示每个线程组有 8 * 8 * 1 = 64 个线程
// 2. C# 侧调用 Dispatch 时传入的 x、y、z 决定一共有多少个线程组
// 比如:
// [numthreads(8, 8, 1)]
// Dispatch(kernel, 64, 64, 1);
// 表示一共会开启 (64*8, 64*8, 1*1) = (512, 512, 1) = 512*512*1 = 262144 个线程
// 也就是说它们共同决定了线程总数
// 总线程数 = numthreads(x,y,z) * Dispatch(groupsX,groupsY,groupsZ)
// = (groupsX * x, groupsY * y, groupsZ * z)
// = groupsX * x * groupsY * y * groupsZ * z
// 注意:
// 每个轴的线程或线程组数不能设置为 0,最小为 1
// 如果是 1D 任务(一维数据,比如处理线性数组计算):x,1,1
// 如果是 2D 任务(二维数据,比如处理图像、纹理等数据):x,y,1
// 如果是 3D 任务(三维数据,比如处理体积型数据,3D 纹理、网格等数据):x,y,z
// x,y,z >= 1
#endregion
#region 知识点三 数值规则
// 1. [numthreads(x, y, z)] 的规则
// 三个参数都是常量整数
// 取值范围:
// 1-1. 每个维度 >= 1
// 1-2. 每个维度 <= 1024
// 1-3. x*y*z <= 1024
// 常见组合:
// (8,8,1)、(16,16,1)、(32,1,1)
// 2. Dispatch(groupsX, groupsY, groupsZ) 的规则
// 三个参数都是整数
// 取值范围:
// 1. 每个维度 >= 1,不能为 0
// 2. 没有硬性上限(取决于 GPU 支持的最大 Dispatch 数量),但通常非常大(几十万没问题)
#endregion
#region 知识点四 匹配规则
// 当我们要处理二维纹理、数组、体积数据时
// 应该让线程数覆盖整个数据区域
// 一般情况下,线程数和分组数的公式如下
// int groupsX = Mathf.CeilToInt(width / (float)numThreadX);
// int groupsY = Mathf.CeilToInt(height / (float)numThreadY);
// int groupsZ = Mathf.CeilToInt(depth / (float)numThreadZ);
// groups:Dispatch 传入的值
// numThread:numthreads 传入的值
// width、height、depth:代表你要处理的数据维度数量
// CeilToInt:向上取整的目的是为了即使不是整数倍也能完全覆盖
// 超出部分的线程可以在 HLSL 中用边界判断过滤掉
// 举例说明:
// 比如我们要利用 ComputeShader 处理一个 512*512 像素的二维图像
// numthreads 为:[numthreads(8, 8, 1)]
// 那么在 C# 侧
// int groupsX = Mathf.CeilToInt(512f / 8) = 64;
// int groupsY = Mathf.CeilToInt(512f / 8) = 64;
// Dispatch(kernel, groupsX, groupsY, 1);
// 实际启动的线程为
// (64*8, 64*8, 1*1) = 512 * 512 = 262144
// 这样就每一个线程处理一个像素,利用 GPU 并行性提升了效率
// 推荐规则
// 1D 数据:
// numthreads - (64,1,1) 或 (128,1,1)
// 2D 数据:
// numthreads - (8,8,1) 或 (16,16,1)
// 3D 数据:
// numthreads - (4,4,4) 或 (8,8,8)
// 主要原因:
// GPU 线程调度单位是 Warp(NVIDIA) / Wavefront(AMD)(32 或 64 个线程)
// 所以最好让 x*y*z 是 32 或 64 的倍数
#endregion
#region 总结
// numthreads 和 Dispatch 中的 xyz 三个轴
// numthreads 定义线程组的大小(最多 1024 个线程)
// Dispatch 定义要开启多少个组,一般按数据尺寸 / numthreads 向上取整计算
// 总线程数 = 它们的乘积
// 所有维度都必须 >= 1
// 1D:处理线性数据(数组、粒子)
// 2D:处理平面数据(图像、屏幕)
// 3D:处理体积数据(3D 纹理、网格)
#endregion
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com