Elo*_*off 18 c++ multithreading x86-64 lock-free stdatomic
我有类似的东西:
if (f = acquire_load() == ) {
... use Foo
}
Run Code Online (Sandbox Code Playgroud)
和:
auto f = new Foo();
release_store(f)
Run Code Online (Sandbox Code Playgroud)
您可以很容易地想象使用atomic with load(memory_order_acquire)和store(memory_order_release)的acquire_load和release_store的实现.但是现在如果release_store是用_mm_stream_si64实现的,这是一个非临时写入,而不是针对x64上的其他商店进行排序的?如何获得相同的语义?
我认为以下是最低要求:
atomic<Foo*> gFoo;
Foo* acquire_load() {
return gFoo.load(memory_order_relaxed);
}
void release_store(Foo* f) {
_mm_stream_si64(*(Foo**)&gFoo, f);
}
Run Code Online (Sandbox Code Playgroud)
并使用它:
// thread 1
if (f = acquire_load() == ) {
_mm_lfence();
... use Foo
}
Run Code Online (Sandbox Code Playgroud)
和:
// thread 2
auto f = new Foo();
_mm_sfence(); // ensures Foo is constructed by the time f is published to gFoo
release_store(f)
Run Code Online (Sandbox Code Playgroud)
那是对的吗?我非常肯定这里绝对需要sfence.但是那个lfence怎么样?是否需要或者简单的编译器障碍对于x64是否足够?例如asm volatile("":::"memory").根据x86内存模型,负载不会与其他负载重新排序.所以根据我的理解,只要存在编译器障碍,acquire_load()必须在if语句中的任何加载之前发生.
我对这个答案中的一些事情可能是错的(来自知道这些东西的人的校对欢迎!).它基于阅读文档和Jeff Preshing的博客,而不是最近的实际经验或测试.
Linus Torvalds强烈建议不要试图发明自己的锁定,因为它很容易弄错.在为Linux内核编写可移植代码时,更多的是一个问题,而不是只有x86的东西,所以我觉得很有勇气尝试为x86解决问题.
使用NT存储的常规方法是连续执行大量操作,例如作为memset或memcpy的一部分,然后是SFENCE一个正常的发布存储到共享标志变量:done_flag.store(1, std::memory_order_release).
使用movnti存储到同步变量会损害性能.您可能希望在Foo其指向的内容中使用NT存储,但是从缓存中逐出指针本身是有悖常理的.(movnt如果存储在高速缓存中,则存储将逐出高速缓存行 ;参见vol1 ch 10.4.6.2高速缓存与非时态数据的高速缓存).
NT存储的整个要点是与非时态数据一起使用,如果有的话,长时间不再使用(通过任何线程).控制访问共享缓冲剂或生产者/消费者使用来标记数据作为读出标志的锁,被预期由其他核读取.
您的函数名称也不能真正反映您正在做的事情.
x86硬件针对正常(非NT)版本存储进行了极大优化,因为每个普通商店都是一个发布商店.硬件必须擅长x86才能快速运行.
使用普通存储/加载只需要访问L3缓存而不是DRAM,以便在Intel CPU上的线程之间进行通信.英特尔的大型包容性 L3缓存可作为缓存一致性流量的后盾.探测来自一个核心的未命中的L3标签将检测另一个核心具有处于已修改或独占状态的高速缓存行的事实.NT存储将需要同步变量一直到DRAM并返回另一个核心来查看它.
movnt商店可以与其他商店重新订购,但不能与较旧的商店重新订购.
英特尔x86手册vol3,第8.2.2章(P6中的内存排序和更新的处理器系列):
- 读取不会与其他读取重新排序.
- 写入不会与较旧的读取重新排序.(注意缺少例外).
- 写入内存不会与其他写入重新排序,但以下情况除外:
- 使用非时间移动指令(MOVNTI,MOVNTQ,MOVNTDQ,MOVNTPS和MOVNTPD)执行的流存储(写入); 和
- 字符串操作(参见第8.2.4.1节).(注意:从我对文档的阅读中,快速字符串和ERMSB操作仍然隐含在开始/结束时有一个StoreStore屏障.只有一个
rep movs或多个商店之间存在潜在的重新排序rep stos.)- ...关于clflushopt和围栏说明的东西
更新:还有一个说明(在8.1.2.2软件控制总线锁定中)说:
不要使用WC内存类型实现信号量.不要对包含用于实现信号量的位置的缓存行执行非临时存储.
这可能只是一个表现建议; 他们没有解释是否会导致正确性问题.请注意,NT存储不是缓存一致的(数据可以位于行填充缓冲区中,即使同一行的冲突数据存在于系统中的其他位置或内存中).也许你可以安全地使用NT商店作为释放店,与常规负载同步,但原子RMW OPS喜欢会遇到问题lock add dword [mem], 1.
释放语义通过程序顺序中的任何读取或写入操作来防止写入释放的内存重新排序.
要阻止与早期商店的重新排序,我们需要一条SFENCE指令,即使对于NT商店也是StoreStore屏障.(并且也是某些类型的编译时重新排序的障碍,但我不确定它是否会阻止先前的负载越过屏障.)正常存储不需要任何类型的屏障指令来进行发布存储,所以你只需要SFENCE在使用NT商店时.
对于负载:WB(回写,即"正常")内存的x86内存模型已阻止LoadStore重新排序,即使对于弱排序的存储,因此我们不需要LFENCE其LoadStore屏障效果,只需要一个LoadStore编译器屏障NT商店.至少在gcc的实现中,std::atomic_signal_fence(std::memory_order_release)即使对于非原子加载/存储,也是一个编译器障碍,但atomic_thread_fence它只是atomic<>加载/存储(包括mo_relaxed)的障碍.使用atomic_thread_fence仍然允许编译器更自由地将加载/存储重新排序为非共享变量. 有关更多信息,请参阅此问答.
// The function can't be called release_store unless it actually is one (i.e. includes all necessary barriers)
// Your original function should be called relaxed_store
void NT_release_store(const Foo* f) {
// _mm_lfence(); // make sure all reads from the locked region are already globally visible. Not needed: this is already guaranteed
std::atomic_thread_fence(std::memory_order_release); // no insns emitted on x86 (since it assumes no NT stores), but still a compiler barrier for earlier atomic<> ops
_mm_sfence(); // make sure all writes to the locked region are already globally visible, and don't reorder with the NT store
_mm_stream_si64((long long int*)&gFoo, (int64_t)f);
}
Run Code Online (Sandbox Code Playgroud)
这存储到原子变量(注意缺少解除引用&gFoo).你的功能存储指向Foo它,这是超级怪异的; IDK的重点是什么.另请注意,它编译为有效的C++ 11代码.
在考虑发布商店的含义时,请将其视为在共享数据结构上释放锁定的商店.在您的情况下,当发布商店变得全局可见时,任何看到它的线程都应该能够安全地取消引用它.
x86不需要任何屏障指令,但指定mo_acquire而不是mo_relaxed为您提供必要的编译器屏障.作为奖励,此功能是可移植的:您将在其他架构上获得任何和所有必要的障碍:
Foo* acquire_load() {
return gFoo.load(std::memory_order_acquire);
}
Run Code Online (Sandbox Code Playgroud)
你没有说任何关于存储gFoo在弱序WC(不可缓存的写入组合)内存中的内容.可能很难安排你的程序的数据段被映射到WC内存......在你映射一些WC视频RAM之后gFoo简单地指向 WC内存会容易得多.但是如果你想从WC内存中获取负载,你可能需要LFENCE.IDK.提出另一个问题,因为这个答案主要假设你正在使用WB内存.
请注意,使用指针而不是标志会创建数据依赖关系.我认为你应该可以使用gFoo.load(std::memory_order_consume),即使在弱排序的CPU(Alpha除外)上也不需要障碍.一旦编译器是足够先进,以确保他们不会破坏数据的依赖,其实可以更好的代码(不提倡mo_consume以mo_acquire使用前阅读了这条mo_consume在生产代码,和ESP.要小心注意,正确测试它是不可能的,因为预计未来的编译器会提供比实际编译器更弱的保证.
最初我认为我们确实需要LFENCE才能获得LoadStore屏障.("写入不能通过早期的LFENCE,SFENCE和MFENCE指令".这反过来又阻止它们在LFENCE之前传递(在变为全局可见之前)读取.
请注意,LFENCE + SFENCE仍然比完整的MFENCE弱,因为它不是StoreLoad屏障.SFENCE自己的文档说它是订购的.LFENCE,但英特尔手册vol3中的x86内存模型表没有提到.如果SFENCE在LFENCE之后才能执行,那么sfence/ lfence实际上可能是一个较慢的等价物mfence,但lfence/ sfence/ movnti将给出释放语义而没有完整的障碍.请注意,在一些后续加载/存储之后,NT存储可能会变得全局可见,这与正常的强排序x86存储不同.)
在x86中,每个加载都具有获取语义,但来自WC内存的加载除外.SSE4.1 MOVNTDQA是唯一的非临时加载指令,在普通(WriteBack)内存上使用时,它不是弱排序的.所以它也是一个获取负载(当用在WB内存上时).
请注意,movntdq只有商店表单,而movntdqa只有一个加载表单.但很显然,英特尔不能只调用它们的storentdqa和loadntdqa.他们都有16B或32B的对齐要求,所以放弃a对我来说没有多大意义.我猜SSE1和SSE2已经引入了一些已经使用mov...助记符(如movntps)的NT商店,但直到几年后才在SSE4.1中加载.(第二代Core2:45nm Penryn).
文档说MOVNTDQA不会改变它所使用的内存类型的排序语义.
...如果存储器源是WB(写回)存储器类型,则实现还可以利用与该指令相关联的非时间提示.
处理器对非时间提示的实现不会覆盖有效的内存类型语义,但提示的实现依赖于处理器.例如,处理器实现可以选择忽略该提示并将该指令处理为任何存储器类型的正常MOVDQA.
实际上,当前的英特尔主流CPU(Haswell,Skylake)似乎忽略了从WB内存加载PREFETCHNTA和MOVNTDQA的提示.请参阅当前x86架构是否支持非临时负载(来自"正常"内存)?,以及非临时负载和硬件预取器,它们一起工作吗?更多细节.
此外,如果你正在使用它WC存储器(例如从视频RAM复制,这样英特尔指南中):
由于WC协议使用弱有序的内存一致性模型,如果多个处理器可能引用相同的WC内存位置或者为了使处理器的读取与其他代理的写入同步,则应将MFENCE或锁定指令与MOVNTDQA指令结合使用在系统中.
但是,这并没有说明应该如何使用它.而且我不确定为什么他们会说MFENCE而不是LFENCE来阅读.也许他们谈论的是写入设备内存,读取设备内存的情况,其中存储必须按照负载(StoreLoad屏障)进行排序,而不仅仅是相互之间(StoreStore屏障).
我在Vol3中搜索过movntdqa,并没有得到任何点击(在整个pdf中).3次点击movntdq:所有关于弱排序和内存类型的讨论仅涉及商店.请注意,这LFENCE是在SSE4.1之前很久才引入的.据推测它对某些东西有用,但IDK是什么.对于负载排序,可能只有WC内存,但我还没有读到什么时候有用.
LFENCE似乎不仅仅是弱有序负载的LoadLoad屏障:它也会订购其他指令.(但不是商店的全球可见性,只是他们的本地执行).
来自英特尔的insn ref手册:
具体来说,LFENCE不会执行,直到所有先前的指令都在本地完成,并且在LFENCE完成之前,之后的指令都不会开始执行.
...
LFENCE之后的指令可以在LFENCE之前从内存中获取,但是在LFENCE完成之前它们不会执行.
对于入门rdtsc使用提示LFENCE;RDTSC,以防止其在执行提前前面的指令,当RDTSCP不可用(和弱排序保证是确定:rdtscp不停止执行,从它前面的下面的说明).(CPUID是序列化指令流的常见建议rdtsc).
| 归档时间: |
|
| 查看次数: |
1267 次 |
| 最近记录: |