本文所有的实验针对 GTX980 显卡,Maxwell 架构,计算能力 5.2。
GPU 共享内存是基于存储体切换的架构(bank-switched-architecture)。在 Femi,Kepler,Maxwell 架构的设备上有 32 个存储体(也就是常说的共享内存分成 32 个bank),而在 G200 与 G80 的硬件上只有 16 个存储体。
每个存储体(bank)每个周期只能指向一次操作(一个 32bit 的整数或者一个单精度的浮点型数据),一次读或者一次写,也就是说每个存储体(bank)的带宽为 每周期 32bit。
如下图所示,在一个线程块中申请如下的共享内存:
__shared__ float sData[32][32];
也就是说在上述的 32 * 32 的二维数组共享内存中,每一列对应同一个 bank。
如上图所示,左侧和右侧的都没有发生 bank conflict。而中间的存在 bank conflcit,由于经过最多两次,该 warp 中的线程就都可以得到所要的数据,所有称为 2-way bank conflict,如果同一个 warp 中的所有线程访问一个 bank 中的 32 个不同地址,则需要分 32 次,称为 32-way bank conflict。
如上图所示,左中右均未发生 bank conflict。
依次我们可以总结:只要同一个 warp 的不同线程会访问到同一个 bank 的不同地址就会发生 bank conflict,除此之外的都不会发生 bank conflict。
既然广播是针对同一个 warp 而言的,那么如果不同的 warp 访问同一个 bank 中的同一个地址呢?由于 每个 SM 中有 4 个 warp scheduler (GTX980),可以很好的调度 warp,使其 warp 之间的访问冲突可以充分的隐藏,因此对效率的影响很小,远远小于 warp 内的 bank conflict。至于 warp scheduler 的调度机制,NVIDIA 没有说的特别清楚,可能也是想要开发者不要过于关注于此。
实现定义如下图所示的 32 * 32 线程块,共 1024 个线程,32 个 warp。
申请如 1 中所示的 32 * 32 的共享内存,共 32 个 bank,每个 bank 对应 32 个元素。
代码如下:
int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列坐标
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行坐标
int index = y_id * col + x_id;
__shared__ float sData[BLOCKSIZE][BLOCKSIZE];
if (x_id < col && y_id < row)
{
sData[threadIdx.y][threadIdx.x] = matrix[index];
__syncthreads();
matrixTest[index] = sData[threadIdx.y][threadIdx.x];
}
代码如下:
int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列坐标
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行坐标
int index = y_id * col + x_id;
__shared__ float sData[BLOCKSIZE][BLOCKSIZE];
if (x_id < col && y_id < row)
{
sData[threadIdx.x][threadIdx.y] = matrix[index];
__syncthreads();
matrixTest[index] = sData[threadIdx.x][threadIdx.y];
}
代码如下:
int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列坐标
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行坐标
int index = y_id * col + x_id;
__shared__ float sData[BLOCKSIZE][BLOCKSIZE+1];
if (x_id < col && y_id < row)
{
sData[threadIdx.x][threadIdx.y] = matrix[index];
__syncthreads();
matrixTest[index] = sData[threadIdx.x][threadIdx.y];
}
上述三个小实验的运行时间为:
实验 1.1 :0.052416 ms
实验 1.2 :0.131072 ms
实验 1.3 :0.053280 ms
除去公共代码后的时间为:
实验 1.1 :0.034816 ms
实验 1.2 :0.113472 ms
实验 1.3 :0.035680 ms
结论:
其实只要添加的是奇数列就可以,只不过 1 列是最节省空间(共享内存太宝贵)的。
代码如下:
int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列坐标
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行坐标
int index = y_id * col + x_id;
__shared__ float sData[BLOCKSIZE][BLOCKSIZE];
if (x_id < col && y_id < row)
{
sData[threadIdx.y][threadIdx.x] = matrix[index];
__syncthreads();
float data = 0.0f;
for (int j = 0; j < BLOCKSIZE; j++)
{
data += sData[threadIdx.x][j];
}
matrixTest[index] = data;
}
int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列坐标
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行坐标
int index = y_id * col + x_id;
__shared__ float sData[BLOCKSIZE][BLOCKSIZE+1];
if (x_id < col && y_id < row)
{
sData[threadIdx.y][threadIdx.x] = matrix[index];
__syncthreads();
float data = 0.0f;
for (int j = 0; j < BLOCKSIZE; j++)
{
data += sData[threadIdx.x][j];
}
matrixTest[index] = data;
}
上述两个实验的运行时间如下所示:
实验 2.1 :0.458144 ms
实验 2.2 :0.090848 ms
从上图也可以看出,修改后的带宽相当于修改前的 32 倍。修改后的运行时间也明显得到改善。
代码如下:
int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列坐标
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行坐标
int index = y_id * col + x_id;
__shared__ float sData[BLOCKSIZE][BLOCKSIZE];
if (x_id < col && y_id < row)
{
sData[threadIdx.y][threadIdx.x] = matrix[index];
__syncthreads();
float data = 0.0f;
for (int j = 0; j < 1000; j++)
{
data = sData[threadIdx.y][threadIdx.x];
}
matrixTest[index] = data;
}
代码如下:
int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列坐标
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行坐标
int index = y_id * col + x_id;
__shared__ float sData[BLOCKSIZE][BLOCKSIZE];
if (x_id < col && y_id < row)
{
sData[threadIdx.y][threadIdx.x] = matrix[index];
__syncthreads();
float data = 0.0f;
for (int j = 0; j < 1000; j++)
{
data = sData[0][threadIdx.x];
}
matrixTest[index] = data;
}
上述两个实验的运行时间如下所示:
实验 2.1 :0.053800 ms
实验 2.2 :0.055328 ms
在实验 2.2 中存在明显的不同 warp 间的冲突,但是运行时间差距很小,也就是说 warp 间冲突的影响很小。
通过 visual profiler 可以判断程序中是否存在 bank conflict,在运行 visual profiler 前需要添加 -lineinfo
选项,在 visual studio 中可以设置,如下所示:
在 visual profiler 中分析实验 1.2,结果如下所示,可以直接定位到出现 bank conflict 的行。
我的GitHub