。当目标请求从barrier()返回的时候,源请求必然已经执行了同一个函数(也就是完成共享变量的写入),因此可以安全地读取变量的值了。
第二种类型的同步叫做内存屏障(memory barrier)。内存屏障的最直接的版本就是memoryBarrier()。如果调用memoryBarrier(),那么就可以保证着色器请求内存的写入操作一定是提交到内存端,而不是通过缓冲区(cache)或者调度队列之类的方式。所有发生在memoryBarrier()之后的操作在读取同一处内存的时候,都可以使用这些内存写入的结果,即使是同一个计算着色器的其它请求也是如此。此外,memoryBarrier()还可以给着色器编译器做出指示,让它不要对内存操作重排序,以免因此跨越屏障函数。如果你觉得memoryBarrier()的约束过于严格,那么你的感觉很正确。事实上,memoryBarrier()系列中还有其它不同的内存屏障子函数。memoryBarrier()所做的只是简单地按照某种未定义的顺序(这个说法不一定准确)依次调用这些子函数而已。
memoryBarrierAtomicCounter()函数会等待原子计数器更新,然后继续执行。memoryBarrierBuffer()和memoryBarrierImage()函数会等待缓存和图像变量的写入操作完成。memoryBarrierShared()函数会等待带有shared限定符的变量更新。这些函数可以对不同类型的内存访问提供更为精细的控制和等待方法。举例来说,如果正在使用原子计数器来实现缓存变量的访问,我们可能希望确保原子计数器的更新被通知到着色器的其它请求,但是不需要等待缓存写入操作本身完成,因为后者可能会花费更长的时间。此外,调用memoryBarrierAtomicCounter()允许着色器编译器对缓存变量的访问进行重排序,而不会受到原子计数器操作的逻辑影响。
注意,就算是调用memoryBarrier()或者它的某个子函数,我们依然不能保证所有的请求都到达着色器的同一个位置。为了确保这一点,我们只有调用执行屏障函数barrier(),然后再读取内存数据,而后者应该是在memoryBarrier()之前被写入的。
内存屏障的使用,对于单一着色器请求中内存交换顺序的确立来说并不是必需的。在着色器的某个请求中读取变量的值总是会返回最后一次写入这个变量的结果,无论编译器是否对它们进行重排序操作。
我们介绍的最后一个函数叫做groupMemoryBarrier(),它等价于memoryBarrier(),但是它只能应用于同一个本地工作组的其它请求。而所有其它的屏障函数都是应用于全局的。也就是说,它们会确保全局工作组中的任何内存写入请求都会在提交之后,再继续执行程序。
本文摘自《OpenGL编程指南(原书第8版)》第12章:计算着色器,机械工业出版社出版。
?