CUDA中文教材的翻译太晦涩了,英文文档也全都是长难句,导致内存栅栏非常难理解。在本文中我会重新梳理一遍自己的理解,并用实验验证。
让我们在全局内存中初始化两个变量:
__device__ int X = 1, Y = 2; 然后在线程 i 中修改这两个变量:
__device__ void writeXY()
{
X = 10;
Y = 20;
} 从线程 i 的视角来看,变量X的写入在变量Y的写入之前,这看似是对的,但编译器会发现X和Y之间没有依赖关系,然后把它们优化成并行写入,所以此时如果用另一个并行的线程读取X和Y变量,X在Y之前写入的这个顺序就无法保证。读取操作也是如此,顺序无法得到保障。
读取变量:
__device__ void readXY()
{
int B = Y;
int A = X;
printf("A=%d, B=%d", A, B);
} 假设write和read函数分别由并行的线程 i 和线程 j 运行,此时打印出来可能有四种结果:
A=1, B=2 ; 说明 j 在 i 之前读取了变量,可以接受
A=10, B=20;说明 i 在 j 之前写入了变量,可以接受
A=10, B=2; 说明 i 已经写入A,但没来得及写入B,j 已经读取了变量,可以接受
A=1, B=20; 说明 i 写入顺序被并行化,而B写在了A前面,这是不能接受的情况,因为它可能会误导其他线程
为了避免第四种情况的出现,需要用内存栅栏函数,来保证线程 i 的读写顺序从线程 j 的视角来看不会颠倒。完整代码如下:
#ifndef __CUDACC__
#define __CUDACC__
#endif
#include<iostream>
#include<cuda_runtime.h>
#include<cooperative_groups.h>
namespace cg = cooperative_groups;
__device__ int X = 1, Y = 2;
__device__ void writeXY()
{
X = 10;
__threadfence();
Y = 20;
}
__device__ void readXY()
{
int B = Y;
__threadfence();
int A = X;
printf("A=%d, B=%d\n", A, B);
}
__global__ void kernel(){
cg::thread_block tb = cg::this_thread_block();
if(tb.thread_rank() == 0) writeXY();
if(tb.thread_rank() == 1) readXY();
}
int main(){
cudaLaunchCooperativeKernel(kernel, 1, 2, NULL, 0, 0);
cudaDeviceSynchronize();
return 0;
}
/* 运行结果: A=10, B=20 */ 我们改变一下线程配置,就能出现不同的结果:
__global__ void kernel(){
cg::thread_block tb = cg::this_thread_block();
if(tb.thread_rank() == 0) writeXY();
if(tb.thread_rank() == 32) readXY();
}
int main(){
cudaLaunchCooperativeKernel(kernel, 1, 64, NULL, 0, 0);
cudaDeviceSynchronize();
return 0;
}
/* A=10, B=2 */
__global__ void kernel(){
cg::thread_block tb = cg::this_thread_block();
if(tb.thread_rank() == 0) readXY();
if(tb.thread_rank() == 1) writeXY();
}
int main(){
cudaLaunchCooperativeKernel(kernel, 1, 2, NULL, 0, 0);
cudaDeviceSynchronize();
return 0;
}
/* A=1, B=2 */ 无论线程配置如何改动,都只会出现前三种可以接受的情况,而第四种情况是不可能出现的,因为内存栅栏__threadfence保证了读写顺序。
新版的内存栅栏函数似乎改名了(cuda::atomic_thread_fence),旧版的函数名可能会在未来被弃用。