我正在阅读rigtorp的SPSCQueue的实现,这是一个非常优雅的设计并且具有非常好的基准。
我了解自述文件中描述的大部分设计哲学。我不明白的是
writeIdxCache_
和 readIdxCache_
有什么帮助。
查看以下用于从队列中获取项目的函数
RIGTORP_NODISCARD T *front() noexcept {
auto const readIdx = readIdx_.load(std::memory_order_relaxed);
if (readIdx == writeIdxCache_) {
writeIdxCache_ = writeIdx_.load(std::memory_order_acquire);
if (writeIdxCache_ == readIdx) {
return nullptr;
}
}
return &slots_[readIdx + kPadding];
}
如果我将上面的实现与下面的实现进行比较,有什么好处?
首先使用
writeIdxCache_
有什么好处
RIGTORP_NODISCARD T *front() noexcept {
auto const readIdx = readIdx_.load(std::memory_order_relaxed);
if (readIdx == writeIdx_.load(std::memory_order_acquire)) {
return nullptr;
}
return &slots_[readIdx + kPadding];
}
readIdxCache_
这里我在加载的时候也直接设置了
readIdxCache_
。这种方法是否仍然有效,或者会破坏队列?它比案例0更好还是更差?
RIGTORP_NODISCARD T *front() noexcept {
readIdxCache_ = readIdx_.load(std::memory_order_relaxed);
if (readIdxCache_ == writeIdxCache_) {
writeIdxCache_ = writeIdx_.load(std::memory_order_acquire);
if (writeIdxCache_ == readIdxCache_) {
return nullptr;
}
}
return &slots_[readIdx + kPadding];
}
对于将存储提交到缓存行的 CPU,它需要该缓存行的独占所有权。 (MESI已修改或独占状态)。 如果一个线程正在写入共享变量并且没有其他线程正在读取它,那么事情就会很高效,并且包含它的缓存行可以保持该状态。
但是,如果另一个线程读取它,其 MESI 共享请求会将缓存行从“修改”转换为“共享”,因此写入器在提交下一个存储之前需要读取所有权。 此外,如果写入器自上次读取后已写入,则执行读取的线程将必须等待其加载的缓存未命中(共享请求)。 (读取所有权会使其他核心中缓存行的所有其他副本无效。)
如果没有索引缓存,每次调用
front()
都会读取两个共享变量,包括另一个线程有时写入的变量。 这会在 writeIdx_
上产生争用,因为它必须针对大多数读取和大多数写入发送缓存一致性消息。 (对于作者来说,每次阅读都会在 readIdx_
上引发争议。)
使用索引缓存,
front()
仅偶尔接触writeIdx_
,只有在读取了上次看到的所有队列条目之后。 同样,写入者也会避免经常接触 readIdx_
,因此,如果您要从队列中删除条目,则写入
readIdx_
会非常高效。
case 2: directly set readIdxCache_
readIdxCache_
纯粹是编写器线程本地的。它不能/不应该与读者分享。
readIdxCache_
是一种优化(正如 Peter 指出的那样),用于减少编写者访问读者光标(特别是缓存行)的频率。即一旦设置了 readIdxCache_,写入器现在可以取得进展,直到其光标到达 readIdxCache_ -
在它需要再次命中当前读取器光标(缓存行)以刷新 readIdxCache_ 之前。
除了将读取器的光标带到写入器的一级缓存之外,
这使得读者光标所在的下一个商店更加昂贵
因为它现在必须发出 RFO 才能将共享缓存线带回到独占状态 (MESI)。