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
他添加了一些填充变量。我认为这是为了使它与缓存线对齐以提高性能。
我有一些问题。
((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风格的缓存一致性协议中为独占模式)。当不同的内核试图修改位于同一缓存线上的不同数据时,会浪费时间来回移动整条缓存线,这就是所谓的错误共享。
在您给出的特定示例中,一个核心可以对一个条目(读(共享)
开始的填充意味着
我不确定这项技术是否完全可移植。假设每个
同样的概念适用于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中的变量,
通常在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 } |