opencl:原子命令实现自旋锁(spinlock)的使用限制

原子命令很重要的用途就是互斥(mutexes)。互斥保证了每次只有一个work-item能访问数据。opencl也支持原子命令,在opencl最初始的版本1.0,原子命令是作为扩展功能(opencl extensions)来提供的(参见cl_khr_global_int32_base_atomics,cl_khr_global_int32_extended_atomics)。到opencl1.2以后,原子命令作为Atomic Functions成为opencl的内置函数(built-in function)。
关于原子命令的概念,opencl中原子命令的使用方法不是本文讨论的重点,而是要说说在opencl用原子命令实现的自旋锁(spinlock)的使用限制。

自旋锁(spinlock)

opencl下实现自旋很简单,下面的代码示例了自锁旋的加锁和解锁:

#pragma OPENCL EXTENSION cl_khr_global_int32_base_atomics : enable
__kernel void atom_test(__global int* mutex){
  while(1==atom_cmpxchg(mutex,0,1));//自旋锁加锁,当*mutex为1时,work-item被阻塞
  //。。。。
  atom_xchg(mutex,0);//自旋锁解锁,将*mutex置为0
}

死锁?why?

上面的代码看着挺简单,跟我们在主机端用的自旋锁没什么区别呀。
但是,这段代码在GPU上运行时工作组(work group)中的工作项(work-item)数目大于1的时候,是不能正常工作的,直接导致设备死锁无响应。

要搞清楚为什么简单的自旋锁在kernel中不能正常运行原原因,就要从GPU的中工作项的内存访问机制说起。
我们知道,一个工作组的工作项都是在同一个计算单元(CU)上运行的,对于GPU的工作项来说,读写内存是个很耗时的过程(尤其是全局内存)。为了提高内存读写效率,同一个工作组中的每个工作项的单个的读写内存操作会被计算单元合并成整个工作组的一次内存操作。
换句话说,从计算单元(CU)的角度来看,计算单元(CU)上运行的每个处理元件(PE)的一次内存访问最终都被合并成以计算单元为单位的一次内存操作。
你还可以理解为每个PE(或work-item)都不能独立地访问内存,必须步调一致的同时访问内存。
如果要举个更形象的例子,就像”挷腿跑”比赛
opencl:原子命令实现自旋锁(spinlock)的使用限制_第1张图片

每个队员的双腿是与相邻的队员挷在一起的,所以每个队员并不能独立自由的迈开双腿,必须与全队的保持步调一致全队的跑起来速度才能最快,
对于一般的内存访问这并没有什么问题。但是对于自旋锁就成了问题:
每个PE(或work-item)都不能独立地访问内存,必须步调一致的同时访问内存(而且执行的是原子命令,光想想我的逻辑思维就已经混乱了),会导致它们不能分别执行加锁和解锁的动作,最后的结果就是设备死锁无响应。

总结

在opencl使用自旋锁的原则是:
对于全局内存(global memory)中的mutext变量,每个work-group只能有一个work-item去访问这个自旋锁变量,超过一个work-item访问mutex变量就会造成GPU死锁。从CU的角度来说,就是同一个CU中只能有一个PE去访问自旋锁变量,否则就会造成GPU死锁。并且工作组(work-group)的数目不能超过计算单元(CU)的数量。
对于局部内存(local memory)中的变量,不能使用自旋锁。(因为只允许一个work-item访问这个局部自旋锁变量是没有实际意义的)。

建议:避免使用自旋锁

其实看到自旋锁在opencl上应用有这么多限制,就能想到自旋锁并不适合在opencl kernel中使用。以我最近的惨痛教训来看,在kernel中使用自旋锁,造成kernel执行性能有几个数量级的下降。
如果你在kernel设计中用到了自旋锁,那么你的代码结构很可能是不太合理的。建议你重新审视你的代码,避免用到自旋锁,这就是我最近折腾一个星期得到的教训。

你可能感兴趣的:(opencl)