Ser*_*tch 2 x86 assembly synchronization sse memory-fences
我想将数据存储在一个大型数组中,并_mm256_stream_si256()在循环中调用.据我所知,然后需要一个内存栅栏来使这些更改对其他线程可见._mm_sfence()说的描述
对在此指令之前发出的所有存储器到存储器指令执行序列化操作.保证在程序顺序之前的每个商店指令在程序顺序之后的任何商店指令之前全局可见.
但是,我最近的当前线程存储是否也可以在后续加载指令中看到(在其他线程中)?或者我必须打电话_mm_mfence()?(后者似乎很慢)
更新:我之前看过这个问题:我什么时候应该使用_mm_sfence _mm_lfence和_mm_mfence.那里的答案主要集中在何时使用围栏.我的问题更具体,该问题的答案不太可能解决这个问题(目前不这样做).
UPDATE2:在注释/答案之后,让我们将"后续加载"定义为线程中的加载,该线程随后获取当前线程当前持有的锁.
但是我的近期商店是否也可以在后续加载指令中看到?
这句话没什么意义.加载是任何线程可以看到内存内容的唯一方法.不知道为什么你说"太",因为没有别的.(除非由非CPU系统设备读取DMA.)
商店变得全局可见的定义是任何其他线程中的加载将从中获取数据. 这意味着商店已离开CPU的专用存储缓冲区,并且是包含所有CPU的数据高速缓存的一致性域的一部分.(https://en.wikipedia.org/wiki/Cache_coherence).
CPU总是尝试尽快将存储缓冲区中的存储提交到全局可见的缓存/内存状态.你可以用障碍做的就是让这个线程等到那个以后再进行操作. 在带有流媒体商店的多线程程序中,这当然是必要的,看起来这就是你实际要问的内容.但我认为重要的是要理解即使没有同步,NT存储也可以非常快速地可靠地显示给其他线程.
x86上的互斥锁解锁有时是一个lock add,在这种情况下,这已经成为NT商店的完整栅栏.但是如果你不能排除使用简单商店的互斥实现,那么至少你需要sfence.
普通的x86存储具有发布内存排序语义(C++ 11 std::memory_order_release).MOVNT流媒体商店已经放宽了排序,但是互斥/自旋锁功能以及对C++ 11 std :: atomic的编译器支持基本上忽略了它们. 对于多线程代码,您必须自己对其进行限制以避免破坏互斥锁/锁定库函数的同步行为,因为它们仅同步正常的x86强排序加载和存储.
执行商店的线程中的加载仍将始终看到最近存储的值,即使是movnt商店也是如此.在单线程程序中永远不需要栅栏.无序执行和内存重新排序的基本规则是它永远不会打破在单个线程内按程序顺序运行的错觉.编译时重新排序也是如此:由于对共享数据的并发读/写访问是C++未定义行为,编译器只需保留单线程行为,除非您使用fence来限制编译时重新排序.
MOVNT + SFENCE在生产者 - 消费者多线程或者正常锁定的情况下很有用,其中自旋锁的解锁只是一个发布存储.
生产者线程使用流存储写入一个大缓冲区,然后将"true"(或缓冲区的地址或其他)存储到共享标志变量中.(Jeff Preshing将其称为有效载荷+保护变量).
消费者线程在该同步变量上旋转,并在看到它变为真后开始读取缓冲区.
生成器必须在写入缓冲区后使用sfence,但在写入标志之前,要确保进入缓冲区的所有存储在标志之前是全局可见的.(但请记住,NT商店仍然可以在当前线程的本地可见.)
(使用锁定库函数时,存储的标志是锁.尝试获取锁的其他线程使用的是获取负载.)
std::atomic <bool> buffer_ready;
producer() {
for(...) {
_mm256_stream_si256(buffer);
}
_mm_sfence();
buffer_ready.store(true, std::memory_order_release);
}
Run Code Online (Sandbox Code Playgroud)
asm会是这样的
vmovntdqa [buf], ymm0
...
sfence
mov byte [buffer_ready], 1
Run Code Online (Sandbox Code Playgroud)
如果没有sfence,一些movnt商店可能会延迟到旗帜商店之后,违反了普通非NT商店的发行语义.
如果您知道正在运行的硬件,并且您知道缓冲区总是很大,那么sfence如果您知道消费者总是从前到后读取缓冲区(按照编写的顺序),您可能会跳过它,因此,当用户线程到达缓冲区末尾时,缓冲区末端的存储可能仍然无法在运行生产者线程的CPU核心中的存储缓冲区中进行传输.
(在评论中) "后续"我的意思是稍后发生.
除非您通过使用使生产者线程与使用者同步的内容来限制何时可以执行这些加载,否则无法实现此目的.正如所说的那样,您要求sfence在其执行的瞬间使全局显示NT存储,以便在其他核心上执行后执行1个时钟周期的负载sfence将看到存储. "后续"的理智定义是"在该线程当前持有的锁定的下一个线程中".
围栏比sfence工作更强大:
x86上的任何原子读 - 修改 - 写操作都需要一个lock前缀,这是一个完整的内存屏障(如mfence).
因此,如果您在流媒体商店之后增加一个原子计数器,那么您也不需要sfence.不幸的是,在C++中std:atomic并且_mm_sfence()彼此不了解,并且允许编译器遵循as-if规则来优化原子.因此,很难确定locked RMW指令将完全位于您在生成的asm中所需的位置.
(基本上,如果在C++抽象机器中可以进行某种排序,编译器就可以发出asm,使得它总是以这种方式发生.例如,将两个连续的增量折叠成一个,+=2这样任何线程都不能观察到计数器是奇数.)
尽管如此,默认情况下会mo_seq_cst阻止大量的编译时重新排序,并且当您仅针对x86时,将其用于读取 - 修改 - 写入操作并没有太大的缺点. sfence但是,它非常便宜,因此在一些流媒体商店和locked操作之间试图避免它的努力可能是不值得的.
相关:pthreads诉SSE弱内存排序.该问题的提问者认为解锁一直都会进行lock编辑操作,从而造成sfence多余的操作.
C++编译器不会尝试sfence在流式存储之后为您插入,即使存在std::atomic排序强于的存储操作relaxed.对于编译器来说,如果没有非常保守的话,可靠地实现这一点是非常困难的(例如sfence,在调用者使用原子的情况下,在具有NT存储的每个函数的末尾).
英特尔内在函数早于C11 stdatomic和C++ 11 std::atomic.std::atomic虚假订购商店的假装的实施并不存在,所以你必须用内在因素自己围住它们.
这似乎是一个很好的设计选择,因为你只想movnt在特殊情况下使用商店,因为它们的缓存驱逐行为.您不希望编译器插入sfence不需要的地方或使用movntifor std::memory_order_relaxed.
| 归档时间: |
|
| 查看次数: |
474 次 |
| 最近记录: |