硬件内存屏障除了提供必要的保证外,是否还能让原子操作的可见性更快?

Does hardware memory barrier make visibility of atomic operations faster in addition to providing necessary guarantees?

TL;DR:在生产者-消费者队列中放置一个不必要的(从 C++ 内存模型的角度来看)内存栅栏或不必要的强内存顺序是否有意义,以便以可能更差的吞吐量为代价获得更好的延迟?


C++ 内存模型在硬件上执行,通过为更强的内存顺序设置某种内存栅栏而不让它们在更弱的内存顺序上。

特别是,如果生产者store(memory_order_release),而消费者使用load(memory_order_acquire)观察存储的值,则加载和存储之间没有栅栏。在 x86 上根本没有栅栏,在 ARM 上栅栏是在存储之前和加载之后进行操作。

没有栅栏存储的值最终会被没有栅栏的负载观察到(可能在几次不成功的尝试之后)

我想知道在队列的两边放置栅栏是否可以更快地观察值? 如果有和没有围栏,延迟是多少?

我希望只有 load(memory_order_acquire)pause / yield 的循环限制在数千次迭代是最好的选择,因为它无处不在,但我想了解为什么。

由于这个问题是关于硬件行为的,我预计没有通用的答案。如果是这样,我想知道的主要是 x86(x64 风格),其次是 ARM。


示例:

T queue[MAX_SIZE]

std::atomic<std::size_t>   shared_producer_index;

void producer()
{
   std::size_t private_producer_index = 0;

   for(;;)
   {
       private_producer_index++;  // Handling rollover and queue full omitted

       /* fill data */;

      shared_producer_index.store(
          private_producer_index, std::memory_order_release);
      // Maybe barrier here or stronger order above?
   }
}


void consumer()
{
   std::size_t private_consumer_index = 0;

   for(;;)
   {
       std::size_t observed_producer_index = shared_producer_index.load(
          std::memory_order_acquire);

       while (private_consumer_index == observed_producer_index)
       {
           // Maybe barrier here or stronger order below?
          _mm_pause();
          observed_producer_index= shared_producer_index.load(
             std::memory_order_acquire);
          // Switching from busy wait to kernel wait after some iterations omitted
       }

       /* consume as much data as index difference specifies */;

       private_consumer_index = observed_producer_index;
   }
}

基本上对内核间延迟没有显着影响, 并且绝对不值得使用 "blindly" 不仔细分析,如果您怀疑以后的加载可能存在任何争用缓存中丢失。

一个常见的误解是需要 asm 屏障才能使存储缓冲区提交到缓存。 事实上,障碍只是让这个核心等待已经发生的事情,然后再加载and/or存储.对于一个完整的屏障,阻塞后面的加载和存储,直到存储缓冲区被耗尽。

std::atomic 之前的糟糕日子里,编译器障碍 是阻止编译器将值保存在 寄存器中的一种方法(专用于 CPU 核心/线程,不连贯),但这是一个编译问题,而不是 asm。 CPU 具有非一致缓存的理论上是可能的(其中 std::atomic 需要进行显式刷新以使存储可见),但是 in practice no implementation runs std::thread across cores with non-coherent caches.


高度相关,我基本上已经写过至少几次这个答案。 (但这看起来是一个专门回答这个问题的好地方,而无需深入讨论哪些障碍会做什么。)


阻止可能与 RFO 竞争的后续加载可能会产生一些非常小的副作用(为了使该核心获得对缓存行的独占访问权以提交存储) . CPU 总是尝试尽可能快地耗尽存储缓冲区(通过提交到 L1d 缓存)。一旦存储提交到 L1d 缓存,它就会对所有其他内核全局可见。 (因为他们是连贯的;他们仍然必须提出分享请求...)

让当前核心将一些存储数据写回到 L3 缓存(尤其是在共享状态下)可以减少未命中惩罚,如果另一个核心上的负载在该存储提交后发生。但是没有好的方法可以做到这一点。 如果除了为下一次读取创建低延迟之外,生产者性能并不重要,那么可能会在 L1d 和 L2 中遗漏。

在 x86 上,Intel Tremont (low power Silvermont series) will introduce cldemote (_mm_cldemote) 将一行写回到外部缓存,但不会一直写到 DRAM。 (clwb 可能会有所帮助,但确实会强制存储一直到 DRAM。此外,Skylake 实现只是一个占位符,其工作方式与 clflushopt 类似。)

  • (不可能)

有趣的事实:PowerPC 上的 non-seq_cst stores/loads 可以在同一物理内核上的逻辑内核之间进行存储转发,使存储对 一些 其他内核可见在它们对 所有 其他核心全局可见之前。这是 AFAIK 线程不同意所有对象的全局存储顺序的唯一真正的硬件机制。 。在其他 ISA 上,包括 ARMv8 和 x86,保证存储对所有其他内核同时可见(通过提交到 L1d 缓存)。


对于加载,CPUs 已经将需求加载优先于任何其他内存访问(因为当然执行必须等待它们。)加载前的障碍只能拖延了。

这可能恰好是最佳的时间巧合,如果这使得它看到它正在等待的存储而不是去 "too soon" 并看到旧的缓存无聊值。但通常没有理由假设或预测 pause 或屏障可能是加载前的好主意。

负载后的屏障也不应该有帮助。稍后的加载或存储可能能够开始,但无序的 CPUs 通常以最旧的优先级进行操作,因此稍后的加载可能无法在此加载机会之前填满所有未完成的加载缓冲区使其加载请求发送到核外(假设缓存未命中,因为最近存储了另一个核。)

我想我可以想象如果这个加载地址有一段时间没有准备好(指针追逐情况)并且当地址时最大数量的非核心请求已经在运行中,那么以后的障碍会有好处确实出名了。

任何可能的好处几乎肯定是不值得的;如果有很多独立于此负载的有用工作,它可以填满所有非核心请求缓冲区(Intel 上的 LFB),那么它很可能不在关键路径上,让这些负载运行可能是一件好事.