rigtorp SPSCQueue索引缓存的作用及相关实现对比疑问
关于SPSCQueue中
writeIdxCache_和readIdxCache_的疑问 我正在研读rigtorp的SPSCQueue实现,这是一款设计优雅、基准测试表现优异的单生产者单消费者队列。我已理解其README中阐述的大部分设计理念,但对writeIdxCache_和readIdxCache_的作用存在疑问。
以下是从队列中获取元素的原函数实现:
case 0: 原实现
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]; }
将上述实现与以下两种实现对比,想了解相关优势:
case 1: 不使用缓存
问题:使用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]; }
回答:
核心优势是减少昂贵的内存屏障操作开销。
std::memory_order_acquire语义的加载操作需要确保后续内存操作不被重排到它之前,且必须从内存同步最新值,相比std::memory_order_relaxed的开销大得多。- 在单生产者单消费者场景中,消费者可能频繁调用
front(),但生产者的writeIdx_并不会每次调用都更新。原实现通过writeIdxCache_缓存writeIdx_的最新值,大多数情况下只需用relaxed语义读取缓存值做比较,只有当缓存值与当前readIdx相等(说明队列可能为空)时,才会执行一次acquire语义的writeIdx_加载。 - 这种设计把高频内存操作从acquire降级为relaxed,大幅降低了内存同步开销,提升了队列在高并发调用下的性能。
case 2: 直接设置readIdxCache_
问题:在此实现中,我在加载时直接设置readIdxCache_,该实现是否可行?是否会破坏队列功能?与case0相比性能更优还是更差?
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]; }
回答:
首先这段代码存在语法错误:return语句中使用了未定义的变量readIdx,编译会直接失败。假设是笔误,替换为readIdxCache_后,我们再分析功能和性能:
功能可行性:
在SPSC队列中,readIdxCache_仅由消费者线程操作(生产者不会触碰它),线程安全层面没问题。修正变量名后,代码逻辑与原实现一致,能正确判断队列是否为空并返回队首元素指针,不会破坏队列核心功能。性能对比:
相比case0,这段代码性能更差:- 原实现中
readIdx是栈上局部变量,加载后直接存于寄存器或栈中,读写开销可忽略。而这段代码每次调用front()都要把readIdx_的值写入readIdxCache_(成员变量,属于内存操作),多了一次relaxed语义的store操作,增加了额外内存开销。 - 缓存
readIdx本身没有收益:readIdx_是消费者自身维护的变量,每次用relaxed加载的开销极低,缓存它无法减少任何昂贵内存操作,反而多了一次写内存的步骤。
- 原实现中
内容的提问来源于stack exchange,提问作者doraemon
相关产品推荐
相关产品推荐

