[英]why memory_order_relaxed performance is the same as memory_order_seq_cst
[英]Why set the stop flag using `memory_order_seq_cst`, if you check it with `memory_order_relaxed`?
Herb Sutter 在他的“原子<>武器”演讲中展示了原子的几个示例用法,其中之一归结为以下内容:(视频链接,时间戳)
一个主线程启动多个工作线程。
工人检查停止标志:
while (.stop:load(std:.memory_order_relaxed)) { // Do stuff. }
主线程最终确实stop = true;
(注意,使用 order= seq_cst
),然后加入工人。
Sutter 解释说使用 order= relaxed
检查标志是可以的,因为谁在乎线程是否以稍大的延迟停止。
但是为什么stop = true;
在主线程中使用seq_cst
? 幻灯片说它是故意不relaxed
的,但没有解释原因。
看起来它会起作用,可能会有更大的停止延迟。
这是性能和其他线程看到标志的速度之间的折衷吗? 即由于主线程只设置了一次标志,我们还不如使用最强的排序,以尽可能快地传递消息?
mo_relaxed
适用于stop
标志的加载和存储更强大的 memory 订单也没有有意义的延迟优势,即使看到对keep_running
或exit_now
标志的更改的延迟很重要。
IDK 为什么 Herb 认为stop.store
不应该放松; 在他的演讲中,他的幻灯片有一条评论说// not relaxed
,但在继续“是否值得”之前,他没有说任何关于商店方面的事情。
当然,负载在工作循环中运行,但存储只运行一次,Herb 真的很喜欢建议坚持使用 SC,除非你有性能方面的原因可以真正证明使用其他东西是合理的。 我希望这不是他唯一的原因; 我发现在尝试了解 memory 订单实际上是必要的以及为什么需要时,这无济于事。 但无论如何,我认为要么是他,要么是他的错误。
ISO C++ 标准没有说明商店在多长时间内可见或有什么影响,只有两个应该建议:第6.9.2.3 节向前进展
18.实现应确保由原子操作或同步操作分配的最后一个值(按修改顺序)将在有限的时间段内对所有其他线程可见。
和33.5.4 顺序和一致性 [atomics.order]仅涵盖原子,不包括互斥锁等:
11.实现应该使原子存储在合理的时间内对原子负载可见。
另一个线程可以在其负载实际看到此存储值之前任意循环多次,即使它们都是seq_cst
,假设它们之间没有任何其他类型的同步。 低线程间延迟是一个性能问题,而不是正确性/正式保证。
而非无限的线程间延迟显然只是一个“应该”的 QOI(实现质量)问题。 :P 标准中没有任何内容表明seq_cst
将有助于存储可见性可能无限期延迟的实现,尽管有人可能会猜测可能是这种情况,例如在具有显式缓存刷新而不是缓存一致性的假设实现中。 (尽管这样的实现在 CPU 的性能方面可能实际上不可用,就像我们现在所拥有的那样;每次释放和/或获取操作都必须刷新整个缓存。)
在真实硬件上(使用某种形式的 MESI 缓存一致性),不同的 memory 存储或加载命令不会使存储更快实时可见,它们只是控制以后的操作是否可以在等待存储提交的同时变得全局可见从存储缓冲区到 L1d 缓存。 (在使该行的任何其他副本无效之后。)
从绝对意义上说,更强的订单和障碍不会让事情发生得更快,它们只会延迟其他事情,直到它们被允许相对于商店或负载发生。 (这是所有真实世界的 CPU AFAIK 的情况;无论如何,它们总是试图让其他内核尽快看到存储,因此存储缓冲区不会填满,并且
另见(我的类似答案):
第二个问答是关于 x86 ,其中从存储缓冲区提交到 L1d 缓存是按程序顺序进行的。 这限制了缓存未命中存储执行的距离,以及在存储之后放置释放或 seq_cst 栅栏以防止以后的存储(和加载)可能竞争资源的任何可能的好处。 (x86 微架构将在存储到达存储缓冲区的头部之前执行 RFO(读取所有权),并且普通加载通常会竞争资源以跟踪我们正在等待响应的 RFO。)但是这些影响在比如退出另一个线程; 只有非常小规模的重新排序。
因为谁在乎线程是否以稍大的延迟停止。
更像是,谁在乎线程是否通过在加载等待检查完成后不进行加载/存储来完成更多工作。 (当然,当我们最终加载true
时,如果它在加载结果的错误推测分支的阴影下,这项工作将被丢弃。)在分支错误预测之后回滚到一致的 state 的成本或多或少独立于在错误预测的分支之外发生了多少已经执行的工作。 它是一个stop
标志,因此其他 CPU 的缓存/内存带宽浪费的工作总量非常少。
这种措辞听起来像是acquire
加载或release
存储实际上会以绝对实时的速度更快地看到存储,而不仅仅是相对于该线程中的其他代码。 (事实并非如此)。
好处是当负载产生false
时,循环迭代中的指令级和内存级并行性更高。 并且简单地避免在获取或特别是 SC 加载需要额外指令的 ISA 上运行额外指令,尤其是昂贵的 2 路屏障指令(如 PowerPC isync
/ sync
或特别是 ARMv7 dmb ish
完全屏障,即使是获取),不像 ARMv8 ldapr
或x86 mov
获取加载指令。 (神箭)
顺便说一句,Herb 是正确的, dirty
标志也可以relaxed
,这只是因为阅读器和任何可能的作者之间的thread.join
同步。 否则,是的,释放/获取。
但在这种情况下, dirty
只需要是atomic<>
,因为可能同时存在的写入器都存储相同的值,ISO C++ 仍然认为是 data-race UB。 例如,因为硬件竞争检测的理论可能性会捕获冲突的非原子访问。 (或者像clang -fsanitize=thread
这样的软件实现)
有趣的事实:C++20 引入了std::stop_token
用作stop
或keep_running
标志。
首先, stop.store(true, mo_relaxed)
在这种情况下就足够了。
launch_workers()
stop = true; // not relaxed
join_workers()';
为什么
stop = true;
在主线程中使用 seq_cst?
Herb 没有提到他使用mo_seq_cst
的原因,但让我们看看几种可能性。
基于“ not relaxed
”的评论,他担心stop.store(true, mo_relaxed)
可以用launch_workers()
或join_workers()
重新排序。
由于launch_workers()
是一个释放操作,而join_workers()
是一个获取操作,因此两者的排序约束不会阻止存储向任一方向移动。
但是,重要的是要注意,对于这种情况,要stop
的存储是使用mo_relaxed
还是mo_seq_cst
。 即使使用最强的排序mo_seq_cst
(由于没有其他 SC 操作不比mo_release
强),排序规则仍然允许使用join_workers()
重新排序。
当然,这种重新订购不会发生,但我的观点是,商店中更严格的订购限制不会产生影响。
他可以认为顺序一致(SC)存储是一个优势,因为执行宽松负载的线程将更快地获取新值(SC 存储刷新存储缓冲区)。
但这似乎无关紧要,因为存储在创建和加入线程之间,这不是一个紧密的循环,或者正如 Herb 所说:“ ..它是否在代码的性能关键区域中,这种开销很重要?.. "
他还谈到了负载:“ ......你不在乎它什么时候到达...... ”
我们不知道真正的原因,但它可能基于您不使用显式排序参数(这意味着mo_seq_cst
)的编程约定,除非它有所作为,在这种情况下,正如 Herb 解释的那样,只有放松的负载会有所不同。
例如,在弱序 PowerPC 平台上, load(mo_seq_cst)
使用(昂贵的) sync
和(更便宜的) isync
指令, load(mo_acquire)
仍然使用isync
,而load(mo_relaxed)
一个都不使用。 在一个紧密的循环中,这是一个很好的优化。
另外值得一提的是,在主流的X86
平台上, load(mo_seq_cst)
和load(mo_relaxed)
在性能上并没有真正的区别
就我个人而言,我喜欢这种编程风格,当排序参数无关紧要时省略它们,而在它们产生影响时使用它们。
stop.store(true); // ordering irrelevant, but uses SC
stop.store(true, memory_order_seq_cst); // store requires SC ordering (which is rare)
这只是风格问题。对于两个商店,编译器将生成相同的程序集。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.