为什么错误共享仍然影响非原子,但远小于原子?

Ale*_*iev 3 c++ x86 cpu-architecture cpu-cache false-sharing

考虑以下证明错误共享存在的示例:

\n\n
using type = std::atomic<std::int64_t>;\n\nstruct alignas(128) shared_t\n{\n  type  a;\n  type  b;\n} sh;\n\nstruct not_shared_t\n{\n  alignas(128) type a;\n  alignas(128) type b;\n} not_sh;\n\n
Run Code Online (Sandbox Code Playgroud)\n\n

一个线程a以 1 为步长递增,另一个线程以 1 为步长递增b。增量编译为lock xaddMSVC,即使结果未使用。

\n\n

a对于和分开的结构b,几秒钟内累积的值大约是 的not_shared_t十倍shared_t

\n\n

到目前为止的预期结果:单独的缓存行在 L1d 缓存中保持热状态,增加lock xadd吞吐量瓶颈,错误共享是缓存行乒乓球的性能灾难。(编者注:更高版本的 MSVClock inc在启用优化时使用。这可能会扩大竞争与非竞争之间的差距。)

\n\n
\n\n

现在我using type = std::atomic<std::int64_t>;用普通的替换std::int64_t

\n\n

(非原子增量编译为inc QWORD PTR [rcx]。循环中的原子加载恰好阻止编译器将计数器保留在寄存器中,直到循环退出。)

\n\n

达到的计数not_shared_t仍大于 的计数shared_t,但现在少于两倍。

\n\n
|          type is          | variables are |      a=     |      b=     |\n|---------------------------|---------------|-------------|-------------|\n| std::atomic<std::int64_t> |    shared     |   59\xe2\x80\x99052\xe2\x80\x99951|   59\xe2\x80\x99052\xe2\x80\x99951|\n| std::atomic<std::int64_t> |  not_shared   |  417\xe2\x80\x99814\xe2\x80\x99523|  416\xe2\x80\x99544\xe2\x80\x99755|\n|       std::int64_t        |    shared     |  949\xe2\x80\x99827\xe2\x80\x99195|  917\xe2\x80\x99110\xe2\x80\x99420|\n|       std::int64_t        |  not_shared   |1\xe2\x80\x99440\xe2\x80\x99054\xe2\x80\x99733|1\xe2\x80\x99439\xe2\x80\x99309\xe2\x80\x99339|\n
Run Code Online (Sandbox Code Playgroud)\n\n

为什么非原子情况的性能如此接近?

\n\n
\n\n

这是完成最小可重现示例的程序的其余部分。(也在带有 MSVC 的 Godbolt 上,准备编译/运行)

\n\n
std::atomic<bool> start, stop;\n\nvoid thd(type* var)\n{\n  while (!start) ;\n  while (!stop) (*var)++;\n}\n\nint main()\n{\n  std::thread threads[] = {\n     std::thread( thd, &sh.a ),     std::thread( thd, &sh.b ),\n     std::thread( thd, &not_sh.a ), std::thread( thd, &not_sh.b ),\n  };\n\n  start.store(true);\n\n  std::this_thread::sleep_for(std::chrono::seconds(2));\n\n  stop.store(true);\n  for (auto& thd : threads) thd.join();\n\n  std::cout\n    << " shared: "    << sh.a     << \' \' << sh.b     << \'\\n\'\n    << "not shared: " << not_sh.a << \' \' << not_sh.b << \'\\n\';\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Pet*_*des 6

当重新加载其自己的存储值时,非原子内存增量可以从存储转发中受益。即使缓存行无效,这种情况也可能发生。核心知道存储最终会发生,并且内存排序规则允许该核心在其自己的存储变得全局可见之前查看它们。

存储转发为您提供在停止之前存储缓冲区增量数量的长度,而不需要对缓存行进行独占访问来执行原子 RMW 增量

当该核心最终获得高速缓存线的所有权时,它可以在 1/时钟提交多个存储。这比内存目标增量创建的依赖链快 6 倍:约 5 个周期存储/重新加载延迟 + 1 个周期 ALU 延迟。 因此,在非原子情况下,执行只是以 1/6 的速率将新存储放入 SB,而核心拥有它, 这就是为什么共享原子与非共享原子之间没有巨大差距的原因。

当然也会有一些内存排序机被清除;that 和/或 SB full 是错误共享情况下吞吐量较低的可能原因。请参阅以下问题的答案和评论:生产者-消费者在超级兄弟姐妹与非超级兄弟姐妹之间共享内存位置的延迟和吞吐量成本是多少?另一个与此类似的实验。


A lock incorlock xadd强制存储缓冲区在操作之前耗尽,并包括作为操作的一部分提交到 L1d 缓存。这使得存储转发不可能,并且仅当缓存行处于独占或修改的 MESI 状态时才会发生。

有关的: