boost lockfree spsc_queue缓存内存访问

时间:2014-10-23 17:57:09

标签: memory boost cpu-architecture lock-free cpu-cache

我需要非常关注当前多线程项目中的速度/延迟。

缓存访问是我试图更好地理解的东西。而且我还不清楚无锁队列(例如boost :: lockfree :: spsc_queue)如何在缓存级别访问/使用内存。

我已经看过使用过的队列,其中消费者核心需要操作的大对象的指针被推入队列。

如果消费者核心从队列中弹出一个元素,我认为这意味着元素(在这种情况下是一个指针)已经加载到消费者核心的L2和L1缓存中。但是要访问元素,是不是需要通过从L3缓存或互连中查找和加载元素来访问指针本身(如果另一个线程位于不同的cpu套接字上)?如果是这样,简单地发送一个可由消费者处理的对象副本可能会更好吗?

谢谢。

1 个答案:

答案 0 :(得分:9)

C ++主要是按需购买生态系统。

任何常规队列都会让选择存储语义(按值或按引用)。

但是,这次你订购了一些特别的东西:你订购了一个无锁队列。 为了无锁,它必须能够执行所有可观察的修改操作作为原子操作。这自然地限制了可以直接在这些操作中使用的类型。

您可能会怀疑是否有可能超出系统本机寄存器大小的值类型(例如,int64_t)。

好问题。

输入Ringbuffers

实际上,任何基于节点的容器都只需要指针交换用于所有修改操作,这在所有现代架构中都是原子的。 但是,在非原子序列中涉及复制多个不同内存区域的任何事情是否真的会造成无法解决的问题?

没有。想象一下POD数据项的平面数组。现在,如果将数组视为循环缓冲区,则只需要以原子方式维护缓冲区前端和末尾位置的索引。容器可以在内部'脏前端索引'中随时更新,同时在外部前面复制。 (副本可以使用轻松的内存排序)。只有在知道整个副本完成后,才会更新外部前端索引。此更新需要在acq_rel / cst内存顺序 [1]

只要容器能够保护front从未完全包裹并到达back的不变量,这是一个很好的协议。我认为这个想法在Disruptor Library(LMAX成名)中得到了普及。你可以从

获得机械共振
  • 读/写时的线性存储器访问模式
  • 如果你可以使记录大小与(多个)物理缓存行对齐,那就更好了
  • 除非POD包含该记录之外的原始引用
  • ,否则所有数据都是本地数据

Boost的spsc_queue如何实际执行此操作?

  1. 是的,spqc_queue将原始元素值存储在连续的对齐内存块中:(例如来自compile_time_sized_ringbuffer,它是spsc_queue的基础,具有静态提供的最大容量:)

    typedef typename boost::aligned_storage<max_size * sizeof(T),
                                            boost::alignment_of<T>::value
                                           >::type storage_type;
    
    storage_type storage_;
    
    T * data()
    {
        return static_cast<T*>(storage_.address());
    }
    

    (元素类型T甚至不需要POD,但它必须是默认构造和可复制的。)

  2. 是的,读写指针是原子积分值。请注意,boost devs已经注意应用足够的填充来避免读/写索引的缓存行上的 False Sharing :(来自ringbuffer_base):

    static const int padding_size = BOOST_LOCKFREE_CACHELINE_BYTES - sizeof(size_t);
    atomic<size_t> write_index_;
    char padding1[padding_size]; /* force read_index and write_index to different cache lines */
    atomic<size_t> read_index_;
    
  3. 事实上,正如您所看到的,在读取或写入方面只有“内部”索引。这是可能的,因为只有一个写入线程,也只有一个读取线程,这意味着在写入操作结束时只有更多空间而不是预期。

  4. 存在其他一些优化:

    • 支持它的平台的分支预测提示(unlikely()
    • 可以一次推送/弹出一系列元素。如果您需要从一个缓冲区/环形缓冲区虹吸到另一个缓冲区/环缓冲区,这应该可以提高吞吐量,尤其是当原始元素大小不等于缓存行的整数倍时
    • 尽可能使用std :: unitialized_copy
    • 将在实例化时间优化调用琐碎的构造函数/析构函数
    • unitialized_copy将优化为所有主要标准库实现的memcpy(意味着,如果您的架构支持,将使用例如SSE指令)。
  5. 总而言之,我们看到了一个关于环形缓冲区的最佳可能想法

    使用什么

    Boost为您提供了所有选择。您可以选择使您的元素类型成为指向您的消息类型的指针。但是,正如您在问题中提到的那样,这种间接性会降低参考的局部性,可能不是最佳的。

    另一方面,如果复制费用昂贵,则将完整的消息类型存储在元素类型中会变得昂贵。至少尝试使元素类型很好地适应缓存行(通常在Intel上为64字节)。

    因此在实践中,您可能会考虑将常用数据存储在值中,并使用指针引用较少使用的数据(指针的成本将低,除非它被遍历)。

    如果您需要“附件”模型,请考虑为引用数据使用自定义分配器,以便您也可以在那里实现内存访问模式。

    让你的探查者指导你。


    [1] 我想说spsc acq_rel应该可行,但我对细节有点生疏。作为一项规则,我强调不要自己编写无锁代码。我推荐其他人跟随我的例子:)