如何以及何时对齐缓存行大小
How and when to align to cache line size?
在Dmitry Vyukov用c++编写的优秀的有界mpmc队列中参见:http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue
他添加了一些填充变量。我认为这是为了使它与缓存线对齐以提高性能。
我有几个问题。
- 为什么要这样做?
- 它是一个可移植的方法吗always work
- 在什么情况下最好使用
__attribute__ ((aligned (64)))
代替。 -
为什么在缓冲区指针之前填充有助于提高性能?难道不只是指针被加载到缓存中所以它实际上只是指针的大小吗?
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 words),这是16字节,所以环缓冲区变量不应该分散在多个DDR RAM字上。有些系统确实使用64位DDR RAM字,技术上你可以在16位CPU上获得32位DDR RAM字,但在这种情况下会使用SDRAM。
在高性能算法中读取数据时,可能只对最小化所使用的缓存行数感兴趣。就我而言,我开发了世界上最快的整数到字符串算法(比之前最快的算法快40%),我正在优化Grisu算法,这是世界上最快的浮点算法。为了打印浮点数,必须打印整数,所以为了优化Grisu,我实现的一个优化是,我将Grisu的查找表(LUT)缓存线对齐到15行缓存中,这是相当奇怪的,它实际上是这样对齐的。这将从.bss部分(即静态内存)获取lut,并将它们放入堆栈(或堆,但stack更合适)。我没有对这个进行基准测试,但这很好,我学到了很多关于这个的知识,加载值的最快方法是从I -cache而不是d-cache中加载它们。不同之处在于i-cache是只读的,并且有更大的缓存行,因为它是只读的(有一次一位教授引用我的话是2KB)。因此,数组索引会降低性能而不是像这样加载变量:
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);
,这里是更快的std::align替换:
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
}
- 如何理解将半精度指针转换为无符号长指针和相关的内存对齐
- 如何创建一个QTableWidgetItem,用长文本右对齐,左边有省略号
- 我可以检测和更改 gcc/g++ 中结构的当前数据对齐设置吗?
- 64位机器上的C++内存对齐
- 为什么我可以将变量存储在不是其最小对齐方式的倍数的地址?
- 使 std::vector 分配对齐内存的现代方法
- C++ cout 将双精度对齐到精度 2 并正确对齐
- 在 64 位边界上对齐C++结构数组?
- 使用 g++7 构建的代码在访问未对齐的内存时崩溃
- 在 capnp FlatArrayMessageReader 的对齐内存缓冲区中接收 zmq 消息
- 是否值得对齐变量?
- 初始化派生结构的基部分/意外打包派生结构字段以对齐基结构的间隙
- gcc 中的多个对齐属性是否可以用于保证缓存行分隔?
- g++ 如何对齐内存以确保对齐从缓存行的开头开始?
- 对齐非 POD 结构的缓存
- 在使用GCC时,如何使用__Align__中的大小进行缓存对齐
- 由于 FPU 或缓存而与 8 字节边界对齐的双精度
- 缓存行、错误共享和对齐
- 如何以及何时对齐缓存行大小
- 如何确定地址是否与缓存对齐