之前已经描述过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之后的标准可能会通过更新存储器模型来禁止这种情况发生。