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

Mat*_*att 57 c c++ caching

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

他添加了一些填充变量.我认为这是为了使其与高速缓存行对齐以提高性能.

我有一些问题.

  1. 为什么这样做?
  2. 这是一种永远有效的便携式方法吗?
  3. 在什么情况下最好使用__attribute__ ((aligned (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_;
    
    Run Code Online (Sandbox Code Playgroud)

这个概念在gcc下是否适用于c代码?

Phi*_*ler 42

它以这种方式完成,因此修改不同字段的不同内核不必在其缓存之间反弹包含它们的缓存行.通常,对于处理器访问内存中的某些数据,包含它的整个缓存行必须位于该处理器的本地缓存中.如果它正在修改该数据,那么该缓存条目通常必须是系统中任何缓存中的唯一副本(MESI/MOESI样式的缓存一致性协议中的独占模式).当单独的内核尝试修改恰好存在于同一缓存线上的不同数据时,从而浪费时间来回移动整条线,这就是所谓的错误共享.

在您给出的特定示例中,一个核心可以将条目入队(仅读取(共享)buffer_和写入(独占)enqueue_pos_),而另一个核心可以排队(共享buffer_和排除dequeue_pos_),而不会在另一个核心的高速缓存行上停止核心.

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

我不确定这项技术是否完全可移植.假设每个都cacheline_pad_t将自己与64字节(其大小)的高速缓存行边界对齐,因此随后的任何内容将在下一个高速缓存行上.据我所知,C和C++语言标准只需要整个结构,因此它们可以很好地存在于数组中,而不会违反任何成员的对齐要求.(看评论)

attribute方法将更加特定于编译器,但可能会将此结构的大小减半,因为填充将限于将每个元素四舍五入为完整的高速缓存行.如果有很多这样的话,这可能是非常有益的.

同样的概念适用于C和C++.

  • 实际上,没有假设"cacheline_pad_t本身将与64字节对齐;" 实际上不需要对齐.填充只保证唯一的目标,即前后的变量在**不同的**缓存行中. (13认同)
  • @MattH:为了便于携带,C++ 11引入了`std :: aligned_storage`,它允许您需要存储定义的大小和对齐.否则`char [N]`的默认对齐方式为`1`. (11认同)
  • 更现代的C++ 11标准具有`alignas`声明修饰符,可以轻松地执行此操作.几乎所有积极开发的C++编译器都支持这一点. (3认同)

小智 8

当您处理中断或高性能数据读取时,您可能需要对齐缓存行边界,通常每个缓存行 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 是一位教授曾经引用过我的话。)。因此,您实际上会降低数组索引的性能,而不是像这样加载变量:

int faster_way = 12345678;
Run Code Online (Sandbox Code Playgroud)

与较慢的方式相反:

int variables[2] = { 12345678, 123456789};
int slower_way = variables[0];
Run Code Online (Sandbox Code Playgroud)

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

通常在 C++ 中,您将使用该std::align函数。我建议不要使用此功能,因为它不能保证以最佳方式工作。这是对齐缓存行的最快方法,首先我是作者,这是一个无耻的插件:

Kabuki Toolkit 内存对齐算法

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);
Run Code Online (Sandbox Code Playgroud)

这是更快的 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
}
Run Code Online (Sandbox Code Playgroud)

  • 您能否详细说明“它将导致 L1、L2 等或缓存或 DDR RAM 充当低通滤波器”的说法,或者发布一个解释链接?我很难理解信号的频率如何影响这个 (3认同)