CUDA原子操作

考虑当大量的线程需要试图修改一段较小的内存区域的情形,这是(在日常的算法实现中)常发生的现象。当我们试图进行“读取–修改-写入”操作序列的时候,这种情形经常会带来很多麻烦。一个例子是代码d_out[i]++,这代码首先将d_out[i]的原值从存储器中读取出来,然后执行了+1操作,再将结果回写到存储器。然而,如果多个线程试图在同一个内存区域中进行这个操作,则可能会得到错误的结果。

#include <stdio.h>

#define NUM_THREADS 10000
#define SIZE  10
#define BLOCK_WIDTH 100

__global__ void gpu_increment_without_atomic(int *d_a)
{
	// Calculate thread id for current thread
	int tid = blockIdx.x * blockDim.x + threadIdx.x;

	// each thread increments elements wrapping at SIZE variable
	tid = tid % SIZE;
	d_a[tid] += 1;
}

int main()
{
	printf("%d total threads in %d blocks writing into %d array elements\n",
		NUM_THREADS, NUM_THREADS / BLOCK_WIDTH, SIZE);

	// declare and allocate host memory
	int h_a[SIZE];
	const int ARRAY_BYTES = SIZE * sizeof(int);

	// declare and allocate GPU memory
	int * d_a;
	cudaMalloc((void **)&d_a, ARRAY_BYTES);
	//Initialize GPU memory to zero
	cudaMemset((void *)d_a, 0, ARRAY_BYTES);

	gpu_increment_without_atomic << <NUM_THREADS / BLOCK_WIDTH, BLOCK_WIDTH >> >(d_a);

	// copy back the array to host memory
	cudaMemcpy(h_a, d_a, ARRAY_BYTES, cudaMemcpyDeviceToHost);

	printf("Number of times a particular Array index has been incremented without atomic add is: \n");
	for (int i = 0; i < SIZE; i++)
	{
		printf("index: %d --> %d times\n ", i, h_a[i]);
	}

	cudaFree(d_a);
	return 0;
}

内核函数简单地通过d_a[tid]+=1这行代码来增加存储器中元素的值。关键的问题在于,(这行代码)对应的具体内存区域被增加了多少次?线程总数为10 000,数组里只有10个(元素)。通过求余(求模,%)运算,来将这10 000个线程ID对应的索引到这10个元素上去。所以,每个相同的内存中的元素位置将有1 000个线程来进行(+1)的运算。理想状态下,数组中每个位置的元素将被增加1000(次个1)。但是我们从输出结果中看并非如此。
为了解决这个问题,CUDA提供了atomicAdd这种原子操作函数。该函数会从逻辑上保证,每个调用它的线程对相同的内存区域上的“读取旧值-累加-回写新值”操作是不可被其他线程扰乱的原子性的整体完成的。

#include <stdio.h>
#include <cuda.h>
#include <cuda_runtime.h>

#define NUM_THREADS 10000
#define SIZE  10
#define BLOCK_WIDTH 100

__global__ void gpu_increment_atomic(int *d_a)
{
	// Calculate thread id for current thread
	int tid = blockIdx.x * blockDim.x + threadIdx.x;

	// each thread increments elements wrapping at SIZE variable
	tid = tid % SIZE;
	atomicAdd(&d_a[tid], 1);
}

int main()
{
	printf("%d total threads in %d blocks writing into %d array elements\n",
		NUM_THREADS, NUM_THREADS / BLOCK_WIDTH, SIZE);

	// declare and allocate host memory
	int h_a[SIZE];
	const int ARRAY_BYTES = SIZE * sizeof(int);

	// declare and allocate GPU memory
	int * d_a;
	cudaMalloc((void **)&d_a, ARRAY_BYTES);
	//Initialize GPU memory to zero
	cudaMemset((void *)d_a, 0, ARRAY_BYTES);
	
	gpu_increment_atomic << <NUM_THREADS / BLOCK_WIDTH, BLOCK_WIDTH >> >(d_a);
	
	// copy back the array to host memory
	cudaMemcpy(h_a, d_a, ARRAY_BYTES, cudaMemcpyDeviceToHost);
	
	printf("Number of times a particular Array index has been incremented is: \n");
	for (int i = 0; i < SIZE; i++) 
	{ 
		printf("index: %d --> %d times\n ", i, h_a[i]); 
	}
	
	cudaFree(d_a);
	return 0;
}

我们用atomicAdd原子操作函数替换了之前的直接+=操作,该函数具有2个参数:第一个参数是我们要进行原子加法操作的内存区域;第二个参数是该原子加法操作具体要加上的值。在这个代码中,1 000个线程对同一内存区域进行原子+1操作,这1 000次相同区域上的操作,每次都将从逻辑上安全地完整执行。这可能会增加执行时间上的代价。
在main函数中,具有10个元素的数组被初始化成0值,然后传递给了内核,但现在,内核中的代码将执行原子累加操作。所以,这个程序输出的结果将是对的,数组中的每个元素将被累加1000。
如果你测量一下这个程序的运行时间,相比之前的那个简单地在全局内存上直接进行加法操作的程序它用的时间更长。这是因为使用原子操作后程序具有更大的执行代价。可以通过使用共享内存来加速这些原子累加操作。如果线程规模不变,但原子操作的元素数量扩大,则这些同样次数的原子操作会更快地完成。这是因为更广泛的分布范围上的原子操作有利于利用多个能执行原子操作的单元,以及每个原子操作单元上面的竞争性的原子事务也相应减少了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

给算法爸爸上香

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值