title: 什么是存储体冲突?

当一个线程束中的多个线程同时请求访问共享内存中同一存储体(bank)但不同地址的内存时,我们称之为发生了存储体冲突。

当线程访问不同的共享内存存储体时,访问是并行处理的(左图)。当它们都访问同一存储体但不同地址时,访问会被串行化(右图)。

当发生存储体冲突时,不同线程的访问会被串行化。这会大幅降低内存吞吐量,即按整数倍减少,从而无法饱和内存带宽

与其他SRAM缓存存储器类似,流式多处理器中的共享内存被组织成称为"存储体"的组。这些存储体可以同时访问,从而增加带宽。

在GPU中,有32个存储体,每个存储体宽度为4字节,连续的32位字(不是64位;GPU设计时考虑的是32位浮点数和整数)映射到连续的存储体。

地址:  0x00  0x04  0x08  0x0C  0x10  0x14  0x18  0x1C  ...  0x7C
存储体:   0     1     2     3     4     5     6     7   ...    31

地址:  0x80  0x84  0x88  0x8C  0x90  0x94  0x98  0x9C  ...  0xFC

存储体:   0     1     2     3     4     5     6     7   ...    31

相差32 × 4 = 128字节的地址会映射到同一存储体。共享内存的容量大致在千字节级别,因此多个地址会映射到同一存储体。

如果我们在共享内存中访问数组的连续元素,线程束中的每个线程都会命中不同的存储体:

__shared__ float data[1024];  // 共享内存中的数组

// 所有32个线程访问data的连续元素
int tid = threadIdx.x;
float value = data[tid];  // 地址最低有效位: 0x00, 0x04, 0x08, ...

所有32次访问在一个内存事务中完成,因为每个线程都命中了不同的存储体。这在上图的左侧有所描绘。

但假设我们希望线程访问行优先共享内存数组中的一列,每行有32个元素,于是我们这样写:

float value = data[tid * 32];  // 地址最低有效位: 0x000, 0x080, 0x100 ...
// 注意:浮点数宽度为4字节

如上图右侧所示,所有访问都命中了同一存储体(存储体0),因此必须串行化,导致延迟增加了32倍,从大约十几个周期增加到数百个周期。我们可以通过转置共享内存数组来解决这个存储体冲突。有关解决存储体冲突的更多技术,请参阅GTC 2024的《CUDA编程与性能优化入门》演讲

请注意,如果线程访问同一存储体中的相同地址,即完全相同的数据,则不会发生冲突,因为数据可以进行多播/广播。