下面介绍下OpenCL 1.2中的原子操作。
原子加法
函数原型如下:
原子加法的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与参数val相加,最后将相加后的结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
原子减法
函数原型如下:
原子减法的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与参数val相减(即*p-val),最后将相减后的结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
原子数据交换
函数原型如下:
原子交换函数的操作数与返回类型可以是int、unsigned int或f loat。其可操作的存储空间可以是全局存储空间,也可以是局部存储空间。此操作的过程很简单,就是将val值存放进参数p所指向的存储地址,然后将p所指向的存储地址中更新之前的值作为返回结果。整个过程是不可被打断的。原子交换操作常用于对某个全局变量进行初始化或是作为互斥体(mutex)使用。
原子递增
函数原型如下:
原子递增的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与1相加(即*p + 1),最后将相加后的结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
原子递减
函数原型如下:
原子递减的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与1相减(即*p – 1),最后将相减后的结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
原子比较与交换
函数原型如下:
原子比较与交换的操作数与返回类型可以是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作与原子交换操作比较类似,而有所不同的是,此操作是先用参数cmp的值与p所指的存储地址的内容进行比较,如果相等,则将val参数的值写到p所指的存储地址中去,否则p所指的存储地址的内容还是原来的值。结果返回p所指的存储地址中更新之前的值。这个原子操作与典型的比较与交换操作相比要鸡肋很多。因为它的返回值无法体现出值的交换是否真的发生,以至于适用范围大大缩减。不过在OpenCL 2.0中,新增的atomic_compare_exchange_strong函数的返回值就是布尔类型,能体现出比较结果。所以,我们将在OpenCL 2.0的原子操作中详细介绍原子比较与交换的操作。
原子求最小值
函数原型如下:
原子求最小值的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与参数val比较获得最小值,最后将最小值结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
原子求最大值
函数原型如下:
原子求最大值的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与参数val比较获得最大值,最后将最大值结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
原子按位与
函数原型如下:
原子按位与的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与参数val做按位与操作(即*p & val),最后将结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
原子按位或
函数原型如下:
原子按位或的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与参数val做按位或操作(即*p | val),最后将结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
原子按位异或
函数原型如下:
原子按位异或的操作数与返回类型是int或unsigned int,而可操作的存储空间可以是全局存储空间也可以是局部存储空间。此操作过程为:将参数p所指的地址内容取出,然后与参数val做按位异或操作(即*p^val),最后将结果再写回p所指的地址中,然后返回原来修改前的p所指地址的内容。整个操作是原子的,即不可被打断的。
在OpenCL 1.2中已经定义了那么多种类丰富的原子操作。下面我们将通过一个简单的例子来介绍一下原子加法的使用。这个例子就是上述计算向量内积的更通用的版本。例如,要计算一对4096个元素向量的内积,我们仍然是先计算一个工作组中所有工作项对应两个向量数据元素的积,然后先保存到局部存储空间。最后每个工作组的第一个工作项计算所有当前工作组所有局部数据中数据元素的和,然后用原子加法把该结果与目的输出存储空间的值相加。我们将上述代码从program = clCreateProgramWithSource(context, 1,(const char**)&kernelSource,(const size_t*)&kernelLength, &ret);语句一直到FINISH标签之前,替换为以下代码:
在主机端代码,我们是将两个长度为16 × 1024 × 1024的int类型数据元素的向量准备做内积操作。我们在内核代码中定义了GROUP_NUMBER_OF_WORKITEMS宏来指定每个工作组包含多少工作项。尽管我们可以在内核程序中使用get_local_size内建函数来获得当前工作组包含多少工作项,但是由于OpenCL C中不能像C99那样定义可变长数组。因此,定义数组长度时必须用编译阶段即可确定的常量。所以我们这里使用宏来指定。下面给出内核程序:
对两个向量求内积的过程之前已经描述了。这里,对输出结果地址的内容求和再写回去必须使用原子操作,如果大家把“atomic_add(pDst, sum);”这条语句屏蔽掉,而换用下面的“pDst[0]+=sum;”,那么结果就很有可能是不正确的。
在这个demo中,我们不知不觉其实已经涉及了不同工作组之间的数据同步。在这个例子中,每个工作组的头一个工作项都会将本工作组计算好的求和结果与输出地址的内容相加,然后再写回去。由于OpenCL并没有提供任何对不同工作组之间工作项的栅栏同步操作,因此我们对工作组之间的同步往往也是借助于原子操作或是以原子操作作为技术前提的同步原语(synchronization primitive)。