1

考虑以下证明虚假共享存在的示例:

using type = std::atomic<std::int64_t>;

struct alignas(128) shared_t
{
  type  a;
  type  b;
} sh;

struct not_shared_t
{
  alignas(128) type a;
  alignas(128) type b;
} not_sh;

一个线程以a1 为增量递增,另一个线程以 1 递增b。使用 MSVC编译增量lock xadd,即使结果未使用。

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

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


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

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

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

|          type is          | variables are |      a=     |      b=     |
|---------------------------|---------------|-------------|-------------|
| std::atomic<std::int64_t> |    shared     |   59’052’951|   59’052’951|
| std::atomic<std::int64_t> |  not_shared   |  417’814’523|  416’544’755|
|       std::int64_t        |    shared     |  949’827’195|  917’110’420|
|       std::int64_t        |  not_shared   |1’440’054’733|1’439’309’339|

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


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

std::atomic<bool> start, stop;

void thd(type* var)
{
  while (!start) ;
  while (!stop) (*var)++;
}

int main()
{
  std::thread threads[] = {
     std::thread( thd, &sh.a ),     std::thread( thd, &sh.b ),
     std::thread( thd, &not_sh.a ), std::thread( thd, &not_sh.b ),
  };

  start.store(true);

  std::this_thread::sleep_for(std::chrono::seconds(2));

  stop.store(true);
  for (auto& thd : threads) thd.join();

  std::cout
    << " shared: "    << sh.a     << ' ' << sh.b     << '\n'
    << "not shared: " << not_sh.a << ' ' << not_sh.b << '\n';
}
4

1 回答 1

3

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

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

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

当然也会有一些内存排序机器清除;这和/或 SB 已满是错误共享情况下吞吐量较低的可能原因。请参阅有关生产者-消费者在超同级与非超同级之间共享内存位置的延迟和吞吐量成本是多少?的答案和评论?对于另一个有点像这个的实验。


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

有关的:

于 2020-05-08T11:26:28.137 回答