Ale*_*iev 2 c++ x86 arm cpu-architecture memory-barriers
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;
}
}
Run Code Online (Sandbox Code Playgroud)
基本上对内核间延迟没有显着影响,如果您怀疑缓存中可能会丢失后期加载的任何争用,则绝对不值得在没有仔细分析的情况下“盲目”使用。
一个常见的误解是需要 asm 屏障才能使存储缓冲区提交到缓存。 事实上,障碍只是让这个核心在做以后的加载和/或存储之前等待它自己已经发生的事情。对于完全屏障,阻塞以后的加载和存储,直到存储缓冲区耗尽。 英特尔硬件上的存储缓冲区大小?究竟什么是存储缓冲区?
在过去的糟糕日子里std::atomic
,编译器障碍是阻止编译器将值保存在寄存器中的一种方法(CPU 内核/线程私有,不连贯),但这是编译问题,而不是 asm。具有非一致性缓存的 CPU 理论上是可能的(其中 std::atomic 需要进行显式刷新以使存储可见),但实际上没有实现跨具有非一致性缓存的内核运行 std::thread。
如果我不使用围栏,一个核心需要多长时间才能看到另一个核心的写入?是高度相关的,我之前至少写过几次这个答案。(但这看起来是一个专门回答这个问题的好地方,而无需深入了解哪些障碍会做什么。)
阻塞可能会与 RFO 竞争的后续加载可能会产生一些非常小的次要影响(该内核可以独占访问缓存行以提交存储)。CPU 总是尝试尽可能快地耗尽存储缓冲区(通过提交到 L1d 缓存)。一旦存储提交到 L1d 缓存,它就会对所有其他内核全局可见。(因为他们是连贯的;他们仍然需要提出共享请求......)
如果另一个核心上的负载发生在此存储提交之后,则让当前核心将一些存储数据写回 L3 缓存(尤其是在共享状态下)可以减少未命中损失。但是没有好的方法可以做到这一点。 如果生产者性能除了为下一次读取创建低延迟之外并不重要,那么可能会在 L1d 和 L2 中创建冲突未命中。
在 x86 上,Intel Tremont(低功耗 Silvermont 系列)将引入cldemote
( _mm_cldemote
) 将一行写回至外部缓存,但不会一直写回 DRAM。(clwb
可能会有所帮助,但确实会迫使商店一直使用 DRAM。此外,Skylake 实现只是一个占位符,其工作方式类似于clflushopt
。)
有趣的事实:PowerPC 上的非 seq_cst 存储/加载可以在同一物理核心上的逻辑核心之间进行存储转发,使存储对其他一些核心可见,然后对所有其他核心全局可见。这是 AFAIK 线程不同意所有对象的全局存储顺序的唯一真正的硬件机制。 其他线程是否总是以相同的顺序看到对不同线程中不同位置的两次原子写入?. 在包括 ARMv8 和 x86 在内的其他 ISA 上,可以保证存储同时对所有其他内核可见(通过提交到 L1d 缓存)。
对于负载,CPU 已经将请求负载优先于任何其他内存访问(因为当然执行必须等待它们。)负载之前的屏障只能延迟它。
如果时间巧合,这可能是最佳的,如果这使它看到它正在等待的存储,而不是“太快”并看到旧的缓存无聊值。但是通常没有理由假设或预测一个pause
或障碍在负载之前可能是一个好主意。
负载后的障碍也不应该有帮助。稍后的加载或存储可能能够启动,但无序 CPU 通常以最旧的优先级执行操作,因此在此加载有机会获得其加载请求之前,稍后的加载可能无法填满所有未完成的加载缓冲区非核心发送(假设缓存未命中,因为最近存储了另一个核心。)
我想我可以想象如果这个加载地址在一段时间内没有准备好(指针追逐情况)并且当地址确实已知时最大数量的非核心请求已经在进行中,我可以想象对以后的障碍的好处。
任何可能的好处几乎肯定是不值得的;如果有很多独立于这个负载的有用工作,它可以填满所有的非核心请求缓冲区(英特尔上的 LFB),那么它很可能不在关键路径上,让这些负载运行可能是一件好事.
归档时间: |
|
查看次数: |
192 次 |
最近记录: |