OpenCL 原子操作的存储器次序规则

之前已经描述过OpenCL 2.0使用了5种存储器次序。
memory_order_relaxed:对存储器次序不做任何约束。
memory_order_release、memory_order_acq_rel以及memory_order_seq_cst:一个存储操作对其所作用受影响的存储器位置执行一次释放操作。
对于memory_order_acquire、memory_order_acq_rel以及memory_order_seq_cst:一个加载操作对受影响的存储器位置执行一次获得操作。
当某些内建函数与另一个执行单元所执行的其他内建函数进行同步时,在特定情况下对成对出现的释放和获得操作而言是可以做这种同步的。对全局对象M执行一次释放操作的原子操作A,全局同步于对M执行一次获得操作的原子操作B,并且原子操作B所读到的值是由之前的原子操作A释放次序中任一副作用所写的值。本地存储器中对象的原子操作也是类似地遵守这一规则:对本地对象M执行一次释放操作的原子操作A,本地同步于对M执行一次获得操作的原子操作B,并且原子操作B所读到的值是由之前A的释放次序中的任一副作用所写的值。

注意指定了memory_order_relaxed的原子操作仅对存储器次序是松弛的。OpenCL 2.0的实现必须仍然要保证对特定原子对象的任一原子访问,并且所有其他对该原子对象的访问都是不可分割的。即原本原子操作的原子性仍然需要获得保障。例如,在做原子加法操作时,加载原子对象,对取到的值做加法计算,最后将计算好的值存储到该原子对象中,整个过程不允许穿插其他对该原子对象的读写操作。
如果在以下两个条件中满足其中一个条件:

1)所有memory_order_seq_cst操作具有memory_scope_all_svm_devices存储器作用域,并且所有受影响的存储器位置都包含在系统分配的存储器空间中,或者这些存储器位置具有原子支持的细粒度的SVM缓存。
2)所有memory_order_seq_cst操作具有memory_scope_device存储器作用域,并且所有受影响的存储器位置不在系统分配的存储器区域内,也不具有原子支持的细粒度的SVM缓存。
所有受影响的存储器位置以及对那些存储器位置具有适当全局前置发生次序和本地前置发生次序,与修改次序相一致的所有memory_order_seq_cst操作应该存在一个总和次序S,以至于从原子对象M加载一个值的每个memory_order_seq_cst操作B,在全局或本地存储器观察到以下值的其中之一:
1)在次序S中发生在操作B之前的对M的最后一次修改操作A的结果,如果操作A存在。
2)如果A存在,那么在关于操作B可见副作用次序中对M的某个修改的结果(例如,如果操作B是一个原子加法,那么这里对M的某个修改结果就是最后把加法计算后的结果写到原子对象M中)。
3)如果A不存在,那么在不是memory_order_seq_cst次序操作B的可见副作用次序中,是对M的某个修改结果。
设X和Y是两个memory_order_seq_cst次序操作,如果X全局同步于或本地同步于Y,那么X同时全局同步和本地同步于Y。

如果总和次序S存在,那么要遵守以下规则:
1)对一个原子操作B来说(B读原子对象M的值),如果有一个memory_order_seq_cst栅栏操作X发生在B之前,那么B要么观察到对M的最近一次发生在X之前的memory_order_seq_cst修改(在总和次序S中),要么在B自己的修改次序中观察到稍后对M的修改。
2)对作用在原子对象M上的原子操作A和B来说(其中A修改了M,而B取M的值),如果有一个memory_order_seq_cst栅栏操作X,使得在次序S中,A发生在X之前,而B发生在X之后,那么B要么观察到A操作的效果,要么在它自己的修改次序中观察到稍后对M的修改。
3)对作用在原子对象M上的原子操作A和B来说(其中A修改了M,而B取其值),如果如果有memory_order_seq_cst栅栏操作X和Y,使得在总和次序S中,A发生在X之前,Y发生在B之前,且X发生在Y之前,那么B要么观察到A操作的效果,要么在其自己的修改次序中观察到稍后对M的修改。
4)对作用在原子对象M的原子操作A和B来说,如果有memory_order_seq_cst栅栏操作X和Y,使得在次序S中,A发生在X之前,Y发生在B之前,且X发生在Y之前,那么B在M的修改次序中发生在A之后。
注意:对原子的读-修改-写操作来说,在修改次序中,应该总是读最近所存储的值,该值是此原子操作中的写操作之前就已存储的。
OpenCL实现应该确保不会计算出“无中生有”的值,在循环依赖于它们自己的计算。对于这条规则,我们观察以下可能会产生问题的例子,这个例子最后计算结果可能为x ==y==42,并且这是一个合法有效的最终状态。

//这里,变量x为全局原子对象
global atomic_int x = ATOMIC_VAR_INIT(0);
//这里,变量y为本地原子对象
local atomic_int y = ATOMIC_VAR_INIT(0);
//执行单元0,之前没有对x和y做任何使用
int t = atomic_load(&y, memory_order_acquire);
atomic_store(&x, t, memory_order_release);
//执行单元1,之前没有对x和y做任何使用
int t = atomic_load(&x, memory_order_acquire);
atomic_store(&y, 42, memory_order_release);

在上述简短的代码里我们看到,执行单元0首先对本地原子对象y做memory_order_acquire次序的原子加载操作,然后把读到的值以memory_order_release存储器次序存储到全局原子对象x中。而在执行单元1中,则先用memory_order_acquire存储器次序对全局原子对象x进行原子加载操作,随后对本地原子对象y用42以memory_order_release次序去写。假设,这里执行单元0先执行,而执行单元1紧接着执行。对执行单元0来说,由于前后分别使用了memory_order_acquire和memory_order_release次序,因此两条语句的发生次序肯定按照其程序次序执行,然而对执行单元1而言,对全局原子对象的加载操作能够看到执行单元0中最后对x的写,但是由于后一条存储操作所用的值不依赖于前一条加载操作,且x与y不处于同一个存储区域,因此虽然执行次序仍然按照程序次序,但这两个操作之间存在一个空隙,使得后一条存储操作不会被阻塞,而是不等上一条原子加载完成就紧接着执行。这样就会发生一个微妙的情况。由于执行单元1对y的存储操作不会受到执行单元0对y的加载操作的约束,再加上对全局存储器的访问本来就要比本地存储器的访问要慢很多,因此可能会先做执行单元1的存储操作。而更有趣的是,执行单元0的获得次序的加载操作具有对释放操作的可见性。因此,在这种微妙的情况下执行单元0获得次序的加载操作完全有可能看到了执行单元1的释放次序的存储操作,这样一来,执行单元0中t的值将会是42。与此同时,在执行单元0中存储到原子对象x的值也将会是42。然后在执行单元1中,获得次序的加载操作能观察到执行单元0最后释放次序的存储操作,使得它的t的值也是42。当然,这个变量t后面没有存储到y中。如果执行单元1中对本地原子对象y存放的t,而不是42,那么就不会存在这种戏剧性的情况了。或者,如果原子对象x和y都在同一个存储区域内(要么都在全局存储空间,要么都在本地存储空间),那么也不会发生这种戏剧性的情况。
这个结果验证了本地前后发生次序与全局前后发生次序之间存在一个“裂缝”周期。当然,这个行为对于OpenCL实现而言不应该暴露给开发者,鼓励开发者使用。OpenCL之后的标准可能会通过更新存储器模型来禁止这种情况发生。

赞(0)
未经允许不得转载:极客笔记 » OpenCL 原子操作的存储器次序规则

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
OpenCL 基本概念
OpenCL 是什么OpenCL 平台模型OpenCL 执行模型OpenCL 上下文简介OpenCL 命令队列简介OpenCL 在设备上执行内核OpenCL 存储器区域OpenCL 存储器对象OpenCL 共享虚拟存储器OpenCL 与OpenGL
OpenCL 基础教程
OpenCL 在Windows上搭建开发环境OpenCL 在Linux上搭建开发环境OpenCL 在OS X上搭建开发环境OpenCL 第一个程序OpenCL 平台OpenCL 设备OpenCL 创建上下文OpenCL 创建命令队列OpenCL 创建Program对象OpenCL 编译Program对象OpenCL 查询和管理Program对象OpenCL 创建内核对象OpenCL 设置内核参数OpenCL 查询和管理内核对象OpenCL 执行内核OpenCL 编写内核代码OpenCL 错误处理
OpenCL C特性
OpenCL 地址空间修饰符OpenCL 函数修饰符OpenCL 对象访问修饰符OpenCL 标量数据类型OpenCL 为什么要有矢量数据类型OpenCL 矢量初始化OpenCL 读取和修改矢量分量OpenCL 运算符OpenCL 维度和工作项OpenCL 工作组OpenCL 矢量数据拷贝OpenCL 异步拷贝和预取OpenCL 数学函数OpenCL 公共函数OpenCL 几何函数OpenCL 整数函数OpenCL 关系函数OpenCL 杂项矢量函数OpenCL 同步函数OpenCL 原子函数OpenCL 内建图像读函数OpenCL 内建无采样器图像读函数OpenCL 内建图像写函数OpenCL 内建图像查询函数OpenCL 工作组函数OpenCL 内建管道读/写函数OpenCL 内建工作组管道读/写函数OpenCL 内建管道查询函数OpenCL 设备队列OpenCL Blocks语法OpenCL 设备队列相关函数OpenCL 子内核存储器可见性OpenCL 设备队列的使用示例
OpenCL 存储器对象
OpenCL 存储器对象OpenCL 分配缓冲区对象OpenCL 创建子缓冲区对象OpenCL 图像对象和采样器对象OpenCL 图像对象OpenCL 图像格式描述符OpenCL 图像描述符OpenCL 图像对象查询OpenCL 采样器对象OpenCL 主机端采样器对象OpenCL 设备端采样器对象OpenCL 图像旋转示例OpenCL 管道OpenCL 创建管道对象OpenCL 管道对象查询OpenCL 主机与设备间数据传输OpenCL 图像对象主机与设备间数据拷贝OpenCL 缓冲区对象数据填充OpenCL 图像对象数据填充OpenCL 缓冲区对象间数据传输OpenCL 图像对象和缓冲区对象间数据拷贝OpenCL 缓冲区对象映射OpenCL 图像对象映射OpenCL 解映射OpenCL 共享虚拟存储器OpenCL SVM缓冲创建与释放OpenCL SVM缓冲映射与解映射OpenCL SVM缓冲填充与拷贝OpenCL SVM类型OpenCL SVM特性OpenCL 共享虚拟存储器示例OpenCL 存储器一致性模型OpenCL 存储器次序规则OpenCL 原子操作的存储器次序规则OpenCL 栅栏操作的存储器次序规则OpenCL 工作组函数的存储器次序规则OpenCL 主机端与设备端命令的存储器次序规则OpenCL 关于存储器次序在实际OpenCL计算设备中的实现
OpenCL 同步及事件机制
OpenCL 同步及事件机制OpenCL 主机端的OpenCL同步OpenCL OpenCL事件机制OpenCL 对OpenCL事件的标记和栅栏OpenCL 内核程序中的同步OpenCL 工作组内同步OpenCL 原子操作OpenCL 1.2中的原子操作OpenCL 2.0中的原子操作OpenCL 局部存储器与全局存储器间的异步拷贝OpenCL 工作组间同步
OpenCL 与OpenGL互操作
OpenCL 与OpenGL互操作OpenCL 从一个OpenGL上下文来创建OpenCL上下文OpenCL 使用OpenGL共享的缓存对象OpenCL 使用OpenGL纹理数据OpenCL 共享OpenGL渲染缓存OpenCL 从一个OpenCL存储器对象查询OpenGL对象信息OpenCL 访问共享对象的OpenCL与OpenGL之间的同步OpenCL AMD Cayman架构GPUOpenCL AMD GCN架构的GPUOpenCL NVIDIA CUDA兼容的GPUOpenCL NVIDIA GPU架构的执行模型OpenCL NVIDIA GPU的全局存储器OpenCL NVIDIA GPU的局部存储器OpenCL ARM Mali GPU硬件架构OpenCL ARM Mali GPU存储器层次OpenCL ARM Mali GPU OpenCL映射