先简单说一下 CPU 和 GPU 的架构不同点。

架构的差异
CPU 有很多的 ALU 算数运算单元,GPU 则是更多的流处理器,所以 CPU 适合复杂的运算任务,而 GPU 适合简单的多核简单运算。

一些 GPU 中的概念

  • streaming processor(sp): 最基本的处理单元。GPU 进行并行计算,也就是很多个 SP 同时做处理。现在 SP 的术语已经有点弱化了,而是
    直接使用thread来代替。一个 SP 对应一个 thread。

  • Warp:warp 是SM 调度和执行的基础概念,通常一个 SM 中的 SP(Thread) 会分成几个 Warp (也就是 SP 在 SM 中是进行分组的,物理上进
    行的分组),一般每一个 Warp 中有32个 Thread.这个 Warp 中的32个 Thread(sp) 是一起工作的,执行相同的指令,如果没有这么多 Thread
    需要工作,那么这个 Warp 中的一些 Thread(sp) 是不工作的(每一个线程都有自己的寄存器内存和 Local Memory,一个 Warp 中的线程是同时
    执行的,也就是当进行并行计算时,线程数尽量为32的倍数,如果线程数不上32的倍数的话;假如是1,则 Warp 会生成一个掩码,当一个指令控制器对一个 Warp 单位的线程发送指令时,32个线程中只有一个线程在真正执行,其他31个 进程会进入静默状态。)

  • Streaming Multiprocessor(sm):多个 SP 加上其他的一些资源组成一个 SM, 其他资源也就是存储资源,共享内存,寄储器等。可见,一个
    SM 中的所有SP是先分成 Warp的,是共享同一个 Memory和 Instruction Unit(指令单元)。从硬件角度讲,一个 GPU 由多个 SM 组成(当然
    还有其他部分),一个 SM 包含有多个 SP(以及还有寄存器资源,Shared Memory资源,L1cache,Scheduler,SPU,LD/ST单元等等)

关于 SIMD :

  • SIMD(Single Instruction Multiple Data)即单指令流多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。

CS 的执行时机:

如下图所示,计算着色器不属于渲染管线的一部分,但是计算着色器可以读写渲染管线。
实际
一个线程组运行于一个多处理器之上,因此对于拥有16个多处理器的 GPU 来说,我们至少应该将任务分解为 16 个线程组,以此领每个多处理器都
充分的运转起来。但是,要活的更佳的性能,我们还应当令每个多处理器至少拥有两个线程组,使它能够切换到不同的线程组进行处理,以连续不停的工作。

每个线程组中都有一块共享内存,供组内的线程访问。但是线程并不能访问其他组中的共享内存。同理,同组内的线程间能够进行同步操作,不同组
的线程间却不能实现这一点。

一个线程组含有 n 个线程。硬件实际上会将这些线程分为多个 warp,而且多处理器会以 SIMD32 的方式来处理 warp。每个 CUDA 核心都可处理一个线程。在 D3D 中能够以非 32 倍数指定线程组的大小,但是出于性能原因应该将线程组的大小设置为 32 的倍数。

CS 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

#pragma kernel HorzBlurCS
#pragma kernel VertBlurCS

cbuffer cbSettings
{
//最大模糊半径为5
float gWeights[11];
};

static const int gBlurRadius = 5;

Texture2D gInput;
RWTexture2D<float4> gOutput;

#define N 256
#define CacheSize (N + 2*gBlurRadius)
groupshared float4 gCache[CacheSize];//公共空间

//水平模糊
[numthreads(N, 1, 1)]
void HorzBlurCS(int3 groupThreadID : SV_GroupThreadID,
int3 dispatchThreadID : SV_DispatchThreadID)
{

//当前线程是否在线程组中处于左边界,是的话要负责填充公共空间像素
if (groupThreadID.x < gBlurRadius)
{
//找到左侧像素的索引并限制大于0
int x = max(dispatchThreadID.x - gBlurRadius, 0);
gCache[groupThreadID.x] = gInput[int2(x, dispatchThreadID.y)];
}
//是否处于右边界
if (groupThreadID.x >= N - gBlurRadius)
{
//找到右侧像素索引并限制小于图片长度
int x = min(dispatchThreadID.x + gBlurRadius, gInput.Length.x - 1);
gCache[groupThreadID.x + 2 * gBlurRadius] = gInput[int2(x, dispatchThreadID.y)];
}

//将自己的颜色填充进索引,如果超出图像范围,则用边界的颜色
gCache[groupThreadID.x + gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy - 1)];

// 等待线程组内所有线程同步
GroupMemoryBarrierWithGroupSync();

//
// 现在模糊每个像素
//

float4 blurColor = float4(0, 0, 0, 0);

[unroll]
for (int i = -gBlurRadius; i <= gBlurRadius; ++i)
{
int k = groupThreadID.x + gBlurRadius + i;

blurColor += gWeights[i + gBlurRadius] * gCache[k];
}

gOutput[dispatchThreadID.xy] = blurColor;
}

//垂直模糊
[numthreads(1, N, 1)]
void VertBlurCS(int3 groupThreadID : SV_GroupThreadID,
int3 dispatchThreadID : SV_DispatchThreadID)
{

if (groupThreadID.y < gBlurRadius)
{
int y = max(dispatchThreadID.y - gBlurRadius, 0);
gCache[groupThreadID.y] = gInput[int2(dispatchThreadID.x, y)];
}
if (groupThreadID.y >= N - gBlurRadius)
{
int y = min(dispatchThreadID.y + gBlurRadius, gInput.Length.y - 1);
gCache[groupThreadID.y + 2 * gBlurRadius] = gInput[int2(dispatchThreadID.x, y)];
}

gCache[groupThreadID.y + gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy - 1)];

GroupMemoryBarrierWithGroupSync();

float4 blurColor = float4(0, 0, 0, 0);

[unroll]
for (int i = -gBlurRadius; i <= gBlurRadius; ++i)
{
int k = groupThreadID.y + gBlurRadius + i;

blurColor += gWeights[i + gBlurRadius] * gCache[k];
}

gOutput[dispatchThreadID.xy] = blurColor;
}

unroll 的含义:

  • 添加了unroll标签的for循环是可以展开的,直到循环条件终止,代价是产生更多机器码

然后是几个测试结果

小米 11 青春版 1000 个圆球每帧更新坐标:
坐标

13700K + 4090 50000物体每秒更新坐标 :
坐标02

小米 11 青春版 运行高斯模糊 :
模糊

参考连接 :