关于c ++:如何以及何时对齐缓存行大小?

How and when to align to cache line size?

用C++编写的Dmitry Vyukov优秀的有界MPMC队列参见:http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue

他添加了一些填充变量。我认为这是为了使它与缓存线对齐以提高性能。

我有一些问题。

  • 为什么要这样做?
  • 这是一种可移植的方法吗?总是工作
  • 在什么情况下,最好使用__attribute__
    ((aligned (64)))
  • 为什么缓冲区指针前的填充会有助于提高性能?不仅仅是加载到缓存中的指针,所以它实际上只是指针的大小吗?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    static size_t const     cacheline_size = 64;
    typedef char            cacheline_pad_t [cacheline_size];

    cacheline_pad_t         pad0_;
    cell_t* const           buffer_;
    size_t const            buffer_mask_;
    cacheline_pad_t         pad1_;
    std::atomic<size_t>     enqueue_pos_;
    cacheline_pad_t         pad2_;
    std::atomic<size_t>     dequeue_pos_;
    cacheline_pad_t         pad3_;
  • 这一概念在GCC中对C代码有效吗?


    这样做是为了让修改不同字段的不同内核不必在缓存之间弹出包含这两个字段的缓存线。通常,为了让处理器访问内存中的某些数据,包含该数据的整个缓存线必须位于该处理器的本地缓存中。如果要修改该数据,则该缓存项通常必须是系统中任何缓存中的唯一副本(在mesi/moesi风格的缓存一致性协议中为独占模式)。当不同的内核试图修改位于同一缓存线上的不同数据时,会浪费时间来回移动整条缓存线,这就是所谓的错误共享。

    在您给出的特定示例中,一个核心可以对一个条目(读(共享)buffer_排队,只写(独占)enqueue_pos_排队,而另一个核心(共享buffer_和独占dequeue_pos_排队,而不会在另一个核心拥有的缓存线上停堆。

    开始的填充意味着buffer_buffer_mask_最终位于同一缓存线上,而不是分成两条线,因此需要双倍的内存流量才能访问。

    我不确定这项技术是否完全可移植。假设每个cacheline_pad_t本身将与一个64字节(其大小)缓存线边界对齐,因此无论后面发生什么,它都将位于下一个缓存线上。据我所知,C语言和C++语言标准只要求整个结构,这样它们就可以很好地在数组中生存,而不会违反它们的任何成员的对齐要求。

    attribute方法将更具体地针对编译器,但可能会将此结构的大小减半,因为填充将限于将每个元素四舍五入到一个完整的缓存行。如果一个人有很多这样的东西,那会非常有益。

    同样的概念适用于C以及C++。


    在处理中断或高性能数据读取时,可能需要与缓存线边界对齐(通常每个缓存线64个字节),并且在处理进程间套接字时必须使用这些边界。对于进程间套接字,有一些控制变量不能跨多个缓存线或DDR RAM字分布,否则将导致L1、L2等或缓存或DDR RAM作为低通滤波器工作,并过滤掉中断数据!那太糟糕了!!!!这意味着当你的算法很好的时候你会得到奇怪的错误,它有可能让你发疯!

    DDR RAM几乎总是以128位字(DDR RAM字)读取,即16个字节,因此环缓冲区变量不应分布在多个DDR RAM字上。有些系统确实使用64位DDR RAM字,从技术上讲,您可以在16位CPU上获得32位DDR RAM字,但在这种情况下,可以使用SDRAM。

    在高性能算法中读取数据时,人们可能只对最小化正在使用的缓存线数量感兴趣。在我的例子中,我开发了世界上最快的整数到字符串算法(比以前的最快算法快40%),我正在努力优化grisu算法,这是世界上最快的浮点算法。为了打印浮点数,您必须打印整数,所以为了优化grisu,我实现的一个优化是将grisu的查找表(lut)对齐到15个缓存行中,这很奇怪,它实际上是这样对齐的。这将从.bss部分(即静态内存)获取LUT,并将它们放在堆栈(或堆)上,但堆栈更合适。我还没有对此进行基准测试,但这是一个很好的方法,我了解了很多,加载值的最快方法是从i-cache而不是d-cache加载值。区别在于i-cache是只读的,并且有更大的缓存线,因为它是只读的(2KB是一位教授曾经引用过我的)。因此,您实际上要从数组索引中降低性能,而不是加载这样的变量:

    1
    int faster_way = 12345678;

    与较慢的方式相反:

    1
    2
    int variables[2] = { 12345678, 123456789};
    int slower_way = variables[0];

    不同之处在于,通过从函数开始时偏移到i-cache中的变量,int variable = 12345678将从i-cache行加载,而slower_way = int[0]将使用更慢的数组索引从较小的d-cache行加载。正如我刚刚发现的,这个特别的微妙之处实际上正在减慢我和其他许多整数到字符串算法的速度。我这么说是因为当您不在时,您可能会通过缓存对齐只读数据来优化您正在优化的内容。

    通常在C++中,您将使用EDCOX1×2函数。我建议不要使用这个函数,因为它不能保证以最佳方式工作。以下是与缓存线对齐的最快方法,我是该缓存线的作者,这是一个无伪的插件:

    kabuki工具包内存对齐算法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    namespace _ {
    /* Aligns the given pointer to a power of two boundaries with a premade mask.
    @return An aligned pointer of typename T.
    @brief Algorithm is a 2's compliment trick that works by masking off
    the desired number of bits in 2's compliment and adding them to the
    pointer.
    @param pointer The pointer to align.
    @param mask The mask for the Least Significant bits to align. */

    template <typename T = char>
    inline T* AlignUp(void* pointer, intptr_t mask) {
      intptr_t value = reinterpret_cast<intptr_t>(pointer);
      value += (-value ) & mask;
      return reinterpret_cast<T*>(value);
    }
    } //< namespace _

    // Example calls using the faster mask technique.

    enum { kSize = 256 };
    char buffer[kSize + 64];

    char* aligned_to_64_byte_cache_line = AlignUp<> (buffer, 63);

    char16_t* aligned_to_64_byte_cache_line2 = AlignUp<char16_t> (buffer, 63);

    这里是更快的性病::对齐替换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    inline void* align_kabuki(size_t align, size_t size, void*& ptr,
                              size_t& space) noexcept {
      // Begin Kabuki Toolkit Implementation
      intptr_t int_ptr = reinterpret_cast<intptr_t>(ptr),
               offset = (-int_ptr) & (align - 1);
      if ((space -= offset) < size) {
        space += offset;
        return nullptr;
      }
      return reinterpret_cast<void*>(int_ptr + offset);
      // End Kabuki Toolkit Implementation
    }