如何以及何时对齐到缓存行大小?

在用 C + + 编写的 Dmitry Vyukov 的优秀有界 mpmc 队列中 参见: < a href = “ http://www. 1024cores.net/home/lock-free-rules/queue/bound-mpmc-queue”> http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue

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

我有几个问题。

  1. 为什么要这样做?
  2. 这是一种可移植的方法吗 总能成功
  3. 在什么情况下最好使用 < code > _ _ tribute _ _ ((对齐(64)))
  4. 为什么缓冲区指针之前的填充有助于提高性能?是不是只有指针加载到缓存中所以它实际上只有指针的大小?

    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_;
    

Would this concept work under gcc for c code?

36028 次浏览

这样做是为了不同的内核修改不同的字段,不必在缓存之间跳转包含这两个字段的缓存线路。通常,对于一个处理器访问内存中的某些数据,包含它的整个缓存线路必须位于该处理器的本地缓存中。如果要修改该数据,那么该缓存条目通常必须是系统中任何缓存中的唯一副本(MESI/MOESI 风格的 缓存一致性协议中的独占模式)。当不同的内核试图修改恰好位于同一条缓存线上的不同数据,从而浪费时间来回移动整条线时,这就是所谓的 虚假分享

在您给出的特定示例中,一个内核可以对一个条目进行排队(读取(共享) buffer_并只写入(独占) enqueue_pos_) ,而另一个内核(共享 buffer_和独占 dequeue_pos_)在另一个内核拥有的缓存线路上不会出现任何一个内核停顿的情况。

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

我不确定这种技术是否是完全可移植的

attribute方法将更加特定于编译器,但是可以将这个结构的大小减少一半,因为填充将仅限于将每个元素四舍五入到一个完整的缓存行。如果一个人有很多这样的东西,那将是非常有益的。

同样的概念也适用于 C 和 C + + 。

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

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

One may also just be interested in minimizing the number of cache lines in use when reading data in a high-performance algorithm. In my case, I developed the world's fastest integer-to-string algorithm (40% faster than prior fastest algorithm) and I'm working on optimizing the Grisu algorithm, which is the world's fastest floating-point algorithm. In order to print the floating-point number you must print the integer, so in order optimize the Grisu one optimization I have implemented is I have cache-line-aligned the Lookup Tables (LUT) for Grisu into exactly 15 cache lines, which is rather odd that it actually aligned like that. This takes the LUTs from the .bss section (i.e. static memory) and places them onto the stack (or heap but the Stack is more appropriate). I have not benchmarked this but it's good to bring up, and I learned a lot about this, is the fastest way to load values is to load them from the i-cache and not the d-cache. The difference is that the i-cache is read-only and has much larger cache lines because it's read-only (2KB was what a professor quoted me once.). So you're actually going to degrigate your performance from array indexing as opposed to loading a variable like this:

int faster_way = 12345678;

而不是更慢的方式:

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

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

通常在 C + + 中,您将使用 std::align函数。我建议不要使用这个函数,因为 它不能保证最佳工作。这里有一个最快的方法来对齐缓存线,这是前面的作者,这是一个无耻的插头:

内存对齐算法

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);

这是更快的标准: : 校准替换:

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
}