TL;DR:在生产者-消费者队列中,是否有必要设置不必要的(从C++内存模型角度来看)内存隔离,或者不必要的强内存顺序,以牺牲可能更糟糕的吞吐量而拥有更好的延迟?
C++内存模型是在硬件上执行的,方法是为更强的内存顺序设置某种内存栅栏,而不是在较弱的内存顺序上执行。
特别是,如果生产者使用store(memory_order_release),并且使用者使用load(memory_order_acquire)来观察存储的值,那么负载和存储之间就没有栅栏。在x86上根本没有栅栏,臂上的栅栏是在储存前和加载后放置操作的。
在没有栅栏的情况下储存的值最终会被没有栅栏的负载所观察(可能经过几次失败的尝试)。
我想知道在队列的任何一边设置栅栏是否能使观察到的值更快?如果是的话,有和没有栅栏的延迟是什么?
我希望,只使用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;
}
}发布于 2020-05-04 12:08:52
基本上不会对内核间延迟、和绝对不值得使用“盲目”的情况下进行仔细的分析,如果您怀疑后面的负载在缓存中丢失了什么争用的话。
这是一个普遍的误解,asm障碍是需要使存储缓冲区提交到缓存。事实上,壁垒使这个核心的等待已经在自己的上发生的事情,然后再进行加载和/或存储。对于一个完整的屏障,阻塞以后加载和存储,直到存储缓冲区耗尽。英特尔硬件上存储缓冲区的大小?什么是存储缓冲区?
在std::atomic之前的糟糕日子里,编译器障碍是阻止编译器将值保存在寄存器中的一种方法(对于CPU内核/线程是私有的,而不是一致的),但这是编译问题,而不是asm。理论上,具有非相干缓存的CPU是可能的(在这里,std::原子需要进行显式刷新才能使存储可见),但是在实践中,没有任何实现使用非相干缓存跨核运行std::线程。。
如果我不使用篱笆,一个核心看另一个核心的文字需要多长时间?是高度相关的,我以前至少写过几次这个答案。(但这似乎是一个很好的地方,可以明确地回答这个问题,而不涉及障碍所起的作用。)
可能有一些非常小的次要影响,比如阻塞以后的负载,这可能会与RFO竞争(因为这个核心可以独占地访问缓存行来提交一个存储)。CPU总是尽可能快地尝试耗尽存储缓冲区(通过提交到L1d缓存)。一旦存储提交到L1d缓存,它就会在所有其他内核中成为全局可见的。(因为它们是连贯的;他们仍然需要提出分享请求.)
让当前内核将一些存储数据写回L3缓存(特别是在共享状态下),如果在此存储提交后另一个内核的负载发生,则可能会减少丢失的代价。但是没有好的方法来做到这一点。如果生产者的性能不重要,而不是为下一次读取创建较低的延迟,那么制造冲突在L1d和L2中可能会失败。
在x86上,英特尔特雷蒙特 (低功耗Silvermont系列)将引入cldemote (_mm_cldemote),它会将一行写回外部缓存,但不会一直写到DRAM。(clwb可能会有所帮助,但确实会迫使商店一直使用DRAM。而且,Skylake实现只是一个占位符,类似于clflushopt)。
有趣的事实: PowerPC上的非seq_cst存储/加载可以在同一物理核上的逻辑核之间存储前向存储,使其他一些核在对所有其他核都全局可见之前就可以看到它们。这是AFAIK的唯一真正的硬件机制,线程不能就所有对象的全局存储顺序达成一致。两个原子写入到不同线程中的不同位置是否总是以相同的顺序被其他线程看到?。在其他ISAs上,包括ARMv8和x86,可以保证存储同时对所有其他内核都是可见的(通过提交到L1d缓存)。
对于负载,CPU已经将需求负载优先于任何其他内存访问(当然,执行必须等待它们)。装货前的障碍物只会延迟。
由于时间的巧合,这可能是最理想的选择,如果这让它看到了它等待的商店,而不是“太早”看到旧的缓存无聊的价值。但是,通常没有理由假设或预测pause或屏障在负载之前是一个好主意。
负载后的障碍物也不会起作用。以后的加载或存储可能能够启动,但无序CPU通常以最古老的优先顺序执行任务,因此,在此加载有机会将其负载请求发送出核心之前,以后的加载可能无法填满所有未完成的负载缓冲区(假设缓存丢失是因为最近存储的另一个核心)。
我想,如果这个加载地址暂时没有准备好(追逐指针的情况),并且当地址被知道时,外部核心请求的最大数量已经在运行,我可以想象到这对以后的障碍有什么好处。
几乎可以肯定,任何可能的好处都是不值得的;如果有那么多有用的工作,除了这个负载之外,它可以填满所有的非核心请求缓冲区(英特尔的LFB),那么它很可能就不会在关键的路径上了,让这些负载在飞行中可能是一件好事。
https://stackoverflow.com/questions/61591287
复制相似问题