提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 原子函数
- 完全在GPU中归约
- 性能比较
- 总结
前言
原子函数的合理使用
原子函数
在cuda里,一个线程的原子操作可以在不受其他线程的任何操作的影响下完成对某个(全局或共享内存中)数据的一套“读-改-写”操作,该操作是不可分的。原子函数是对它的第一个参数指向的数据进行一次“读-改-写”的一类原子操作的函数,一气呵成,不可分割。
如:
T atomicAdd(T *address,T val);// 加法
T atomicSub(T *address,T val);// 减法
T atomicExch(T *address,T val);// 交换
T atomicMin(T *address,T val);// 最小值
T atomicMax(T *address,T val);// 最大值
T atomicInc(T *address,T val);// 自增
T atomicDec(T *address,T val);// 自减
T atomicAnd(T *address,T val);// 按位与
T atomicOr(T *address,T val);// 按位或
T atomicXor(T *address,T val);// 按位异或
T atomicCAS(T *address,T camepare,T val);// 比较-交换
第一个参数是带累加变量的地址。第二个参数是累加的值val。该函数的作用是先将地址adress中的旧值old读出,计算old+val,然后将计算的值存入地址adress。
完全在GPU中归约
在共享内存中的数组归约,没有在核函数里做全部的计算,后面线程块的求和是在主机上进行的。为了提升性能,将所有的计算在核函数里完成。有两种方法:
(1)利用另一个核函数将最后那部分在主机里的求和操作完成;
(2)在原来的核函数里利用原子函数代替在主机里的求和操作;
这里主要介绍第二种方法。
在前面博客上讲的的全局和共享内存数组归约的计算中,最后的求和是将数据复制到全局内存d_y,然后在主机上操作:
if (tid == 0)
{
d_y[bid] = s_y[0];
}
如果不用原子函数,为了满足在核函数做最后的求和操作直接修改成
if (tid == 0)
{
// 求和
d_y[0] += s_y[0];
}
这是不能实现的。因为d_y[0] += s_y[0] 这个操作可以分成两步。首先,从d_y[0]中取出数据与s_y[0]相加;然后将结果写入d_y[0]。因为线程执行的异步性,可能会发生一个线程还未将结果写入d_y[0]中,另一个线程就要读取d_y[0]的数据,这样就会发生冲突,导致错误的结果。如果一个线程的“读-写”操作一气呵成,不受其他线程干扰,那么这样的操作就可行。所以,利用原子函数是可以解决这个问题的。
if (tid == 0)
{
// 原子函数
atomicAdd(d_y, s_y[0]);
}
根据前面提到的原子函数的性质,原子函数的操作在一个线程里执行计算是不会被其他线程所干扰的。第一个参数是一个指针变量,可以用数组名d_y也可以使用&d_y[0]。注意一个细节就是要用到d_y[0],所以在前面要记得初始化d_y[0]的值为0。
性能比较
在2080Ti上,使用单精度浮点和双精度浮点运算,使用原子函数进行数组归约和函数的的性能:
(1)单精度
相比前面在共享内存上计算的,提升了大约(4.45-1.85)2.60/4.45~58.4%,性能提升显著。
(2)双精度
相比前面在共享内存上计算的,提升了大约(7.50-3.4)4.1/7.5~54.7%,性能提升显著。
总结
cuda核函数中使用原子函数
参考:
如博客内容有侵权行为,可及时联系删除!
CUDA 编程:基础与实践
https://docs.nvidia.com/cuda/
https://docs.nvidia.com/cuda/cuda-runtime-api
https://github.com/brucefan1983/CUDA-Programming