为什么要使用 `memory_order_seq_cst` 设置停止标志,如果您使用 `memory_order_relaxed` 检查它?
Why set the stop flag using `memory_order_seq_cst`, if you check it with `memory_order_relaxed`?
Herb Sutter 在他的“atomic<> weapons”演讲中展示了几个原子学的使用示例,其中之一归结为以下内容:(video link, timestamped)
一个主线程启动多个工作线程。
工人检查停止标志:
while (!stop.load(std::memory_order_relaxed))
{
// Do stuff.
}
主线程最终做stop = true;
(注意,使用order=seq_cst
),然后加入workers。
Sutter 解释说用 order=relaxed
检查标志是可以的,因为谁在乎线程是否以稍大的延迟停止。
但是为什么主线程中的stop = true;
要用seq_cst
呢?幻灯片说它故意不是 relaxed
,但没有解释 为什么 .
看起来它可以工作,可能有更大的停止延迟。
这是性能和其他线程看到标志的速度之间的折衷吗? IE。由于主线程只设置一次标志,我们不妨使用最强的排序,尽快获得消息?
mo_relaxed
对于 stop
flag
的加载和存储都很好
即使看到 keep_running
或 exit_now
标志更改的延迟很重要,但对于更强的内存顺序, 也没有有意义的延迟优势。
IDK 为什么 Herb 认为 stop.store
不应该放松;在他的演讲中,他的幻灯片有一条关于作业的评论 // not relaxed
,但在继续“是否值得”之前,他没有说任何关于商店方面的事情。
当然,负载在工作循环内运行,但存储只运行一次,Herb 真的很喜欢建议坚持使用 SC,除非您有真正证明使用其他东西的性能原因。我希望这不是他唯一的原因;我发现在试图理解实际上需要什么样的内存顺序以及为什么需要时,这无济于事。但无论如何,我认为要么是他的错误,要么是他的错误。
ISO C++ 标准 没有说明存储多长时间可见或可能影响它的任何内容,只是部分 6.9.2.3 前进进度
18. An implementation should ensure that the last value (in modification order) assigned by an atomic or synchronization operation will become visible to all other threads in a finite period of time.
另一个线程可以在其加载实际看到此存储值之前任意循环多次,即使它们都是 seq_cst
,假设它们之间没有任何其他类型的同步。低线程间延迟是一个性能问题,而不是正确性/正式保证。
非无限线程间延迟显然只是一个“应该”的 QOI(实施质量)问题。 :P 标准中没有任何内容表明 seq_cst
有助于实现商店可见性可能无限期延迟的实现,尽管有人可能会猜测可能是这种情况,例如在具有显式缓存刷新而不是缓存一致性的假设实现上。 (虽然 such an implementation is probably not practically usable 就 CPU 的性能而言与我们现在拥有的任何东西一样;每个版本 and/or 获取操作都必须刷新整个缓存。)
在真实硬件上(使用某种形式的 MESI 缓存一致性),存储或加载的不同内存顺序不会使存储更快地实时可见,它们只是控制在等待存储从存储缓冲区提交到 L1d 缓存的同时,后续操作是否可以变得全局可见。 (在使该行的任何其他副本无效之后。)
更强的命令和壁垒,不会让事情在绝对意义上更快发生,它们只会延迟其他事情,直到它们相对于商店或负载被允许发生。(在所有真实世界的 CPU AFAIK 上都是这种情况;无论如何,它们总是试图让其他内核尽快看到存储,因此存储缓冲区不会填满,并且
另见(我的类似回答):
- Thread synchronization: How to guarantee visibility of writes(在当前的真实硬件上这不是问题)
第二个问答是关于 x86 的,其中从存储缓冲区到 L1d 缓存的提交是按程序顺序进行的。这限制了缓存未命中存储执行可以达到的程度,以及在存储之后放置释放或 seq_cst 栅栏以防止以后的存储(和加载)可能竞争资源的任何可能的好处。 (x86 微体系结构将在存储到达存储缓冲区的头部之前执行 RFO(读取所有权),而普通加载通常会竞争资源以跟踪我们正在等待响应的 RFO。)但这些影响在类似于退出另一个线程;只有非常小规模的重新排序。
because who cares if the thread stops with a slightly bigger delay.
更像是,谁在乎线程是否通过 完成更多工作而不是 loads/stores 在加载后等待检查完成 。 (当然,当我们最终加载 true
时,如果它处于加载结果上错误推测的分支的阴影下,这项工作将被丢弃。)在分支错误预测后回滚到一致状态的成本更高或者更不依赖于在错误预测的分支之外发生了多少已经执行的工作。这是一个 stop
标志,因此占用其他 CPU cache/memory 带宽的浪费工作总量非常小。
这种措辞听起来像是 acquire
加载或 release
存储实际上会更快地以绝对实时的方式看到存储,而不仅仅是相对于此线程中的其他代码。 (事实并非如此)。
好处是当负载产生 false
时,跨循环迭代的指令级和内存级并行性更高。并且简单地避免 运行 ISA 上的额外指令,其中获取或特别是 SC 加载需要额外指令,尤其是昂贵的 2 路屏障指令,不像 ARM64 ldapr
。
顺便说一句,Herb 是对的,dirty
标志也可以是 relaxed
,只是因为 reader 和任何可能的作者之间的 thread.join
同步。否则是的,释放/获取。
但在这种情况下,dirty
根本只需要 atomic<>
,因为可能的同时写入器都存储相同的值,ISO C++ 仍将其视为数据争用 UB。例如因为理论上硬件竞争检测的可能性会陷入冲突的非原子访问。
首先,在这种情况下,stop.store(true, mo_relaxed)
就足够了。
launch_workers()
stop = true; // not relaxed
join_workers()';
why does stop = true;
in the main thread use 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()
.[=58 重新排序=]
当然,这种重新排序不会发生,但我的观点是,商店中更严格的排序限制不会产生影响。
他可以论证顺序一致 (SC) 存储是一个优势,因为执行宽松负载的线程将更快地获取新值
(SC 存储刷新存储缓冲区)。
但这似乎无关紧要,因为商店处于创建和加入线程之间,这并不处于紧密循环中,或者如 Herb 所说:
“..它是否在代码的性能关键区域中,这种开销很重要?..”
他还谈到负载:“..你不在乎它什么时候到达..”
我们不知道真正的原因,但这可能是基于您不使用显式排序参数(这意味着 mo_seq_cst
)的编程约定,除非它有所作为,
在这种情况下,正如 Herb 解释的那样,只有放松的负荷才会有所不同。
例如,在弱排序的 PowerPC 平台上,load(mo_seq_cst)
同时使用(昂贵的)sync
和(便宜的)isync
指令,
load(mo_acquire)
仍然使用 isync
,load(mo_relaxed)
使用其中的 none。在紧密循环中,这是一个很好的优化。
另外值得一提的是,在主流的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)
这只是样式问题。对于这两个商店,编译器将生成相同的程序集。
Herb Sutter 在他的“atomic<> weapons”演讲中展示了几个原子学的使用示例,其中之一归结为以下内容:(video link, timestamped)
一个主线程启动多个工作线程。
工人检查停止标志:
while (!stop.load(std::memory_order_relaxed)) { // Do stuff. }
主线程最终做
stop = true;
(注意,使用order=seq_cst
),然后加入workers。
Sutter 解释说用 order=relaxed
检查标志是可以的,因为谁在乎线程是否以稍大的延迟停止。
但是为什么主线程中的stop = true;
要用seq_cst
呢?幻灯片说它故意不是 relaxed
,但没有解释 为什么 .
看起来它可以工作,可能有更大的停止延迟。
这是性能和其他线程看到标志的速度之间的折衷吗? IE。由于主线程只设置一次标志,我们不妨使用最强的排序,尽快获得消息?
mo_relaxed
对于 stop
flag
的加载和存储都很好
即使看到 keep_running
或 exit_now
标志更改的延迟很重要,但对于更强的内存顺序, 也没有有意义的延迟优势。
IDK 为什么 Herb 认为 stop.store
不应该放松;在他的演讲中,他的幻灯片有一条关于作业的评论 // not relaxed
,但在继续“是否值得”之前,他没有说任何关于商店方面的事情。
当然,负载在工作循环内运行,但存储只运行一次,Herb 真的很喜欢建议坚持使用 SC,除非您有真正证明使用其他东西的性能原因。我希望这不是他唯一的原因;我发现在试图理解实际上需要什么样的内存顺序以及为什么需要时,这无济于事。但无论如何,我认为要么是他的错误,要么是他的错误。
ISO C++ 标准 没有说明存储多长时间可见或可能影响它的任何内容,只是部分 6.9.2.3 前进进度
18. An implementation should ensure that the last value (in modification order) assigned by an atomic or synchronization operation will become visible to all other threads in a finite period of time.
另一个线程可以在其加载实际看到此存储值之前任意循环多次,即使它们都是 seq_cst
,假设它们之间没有任何其他类型的同步。低线程间延迟是一个性能问题,而不是正确性/正式保证。
非无限线程间延迟显然只是一个“应该”的 QOI(实施质量)问题。 :P 标准中没有任何内容表明 seq_cst
有助于实现商店可见性可能无限期延迟的实现,尽管有人可能会猜测可能是这种情况,例如在具有显式缓存刷新而不是缓存一致性的假设实现上。 (虽然 such an implementation is probably not practically usable 就 CPU 的性能而言与我们现在拥有的任何东西一样;每个版本 and/or 获取操作都必须刷新整个缓存。)
在真实硬件上(使用某种形式的 MESI 缓存一致性),存储或加载的不同内存顺序不会使存储更快地实时可见,它们只是控制在等待存储从存储缓冲区提交到 L1d 缓存的同时,后续操作是否可以变得全局可见。 (在使该行的任何其他副本无效之后。)
更强的命令和壁垒,不会让事情在绝对意义上更快发生,它们只会延迟其他事情,直到它们相对于商店或负载被允许发生。(在所有真实世界的 CPU AFAIK 上都是这种情况;无论如何,它们总是试图让其他内核尽快看到存储,因此存储缓冲区不会填满,并且
另见(我的类似回答):
- Thread synchronization: How to guarantee visibility of writes(在当前的真实硬件上这不是问题)
第二个问答是关于 x86 的,其中从存储缓冲区到 L1d 缓存的提交是按程序顺序进行的。这限制了缓存未命中存储执行可以达到的程度,以及在存储之后放置释放或 seq_cst 栅栏以防止以后的存储(和加载)可能竞争资源的任何可能的好处。 (x86 微体系结构将在存储到达存储缓冲区的头部之前执行 RFO(读取所有权),而普通加载通常会竞争资源以跟踪我们正在等待响应的 RFO。)但这些影响在类似于退出另一个线程;只有非常小规模的重新排序。
because who cares if the thread stops with a slightly bigger delay.
更像是,谁在乎线程是否通过 完成更多工作而不是 loads/stores 在加载后等待检查完成 。 (当然,当我们最终加载 true
时,如果它处于加载结果上错误推测的分支的阴影下,这项工作将被丢弃。)在分支错误预测后回滚到一致状态的成本更高或者更不依赖于在错误预测的分支之外发生了多少已经执行的工作。这是一个 stop
标志,因此占用其他 CPU cache/memory 带宽的浪费工作总量非常小。
这种措辞听起来像是 acquire
加载或 release
存储实际上会更快地以绝对实时的方式看到存储,而不仅仅是相对于此线程中的其他代码。 (事实并非如此)。
好处是当负载产生 false
时,跨循环迭代的指令级和内存级并行性更高。并且简单地避免 运行 ISA 上的额外指令,其中获取或特别是 SC 加载需要额外指令,尤其是昂贵的 2 路屏障指令,不像 ARM64 ldapr
。
顺便说一句,Herb 是对的,dirty
标志也可以是 relaxed
,只是因为 reader 和任何可能的作者之间的 thread.join
同步。否则是的,释放/获取。
但在这种情况下,dirty
根本只需要 atomic<>
,因为可能的同时写入器都存储相同的值,ISO C++ 仍将其视为数据争用 UB。例如因为理论上硬件竞争检测的可能性会陷入冲突的非原子访问。
首先,在这种情况下,stop.store(true, mo_relaxed)
就足够了。
launch_workers()
stop = true; // not relaxed
join_workers()';
why does
stop = true;
in the main thread use 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()
.[=58 重新排序=] 当然,这种重新排序不会发生,但我的观点是,商店中更严格的排序限制不会产生影响。他可以论证顺序一致 (SC) 存储是一个优势,因为执行宽松负载的线程将更快地获取新值 (SC 存储刷新存储缓冲区)。
但这似乎无关紧要,因为商店处于创建和加入线程之间,这并不处于紧密循环中,或者如 Herb 所说: “..它是否在代码的性能关键区域中,这种开销很重要?..”
他还谈到负载:“..你不在乎它什么时候到达..”
我们不知道真正的原因,但这可能是基于您不使用显式排序参数(这意味着 mo_seq_cst
)的编程约定,除非它有所作为,
在这种情况下,正如 Herb 解释的那样,只有放松的负荷才会有所不同。
例如,在弱排序的 PowerPC 平台上,load(mo_seq_cst)
同时使用(昂贵的)sync
和(便宜的)isync
指令,
load(mo_acquire)
仍然使用 isync
,load(mo_relaxed)
使用其中的 none。在紧密循环中,这是一个很好的优化。
另外值得一提的是,在主流的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)
这只是样式问题。对于这两个商店,编译器将生成相同的程序集。