繁体   English   中英

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

[英]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

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

我有一些问题。

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

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

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

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

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

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

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

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

在处理中断或高性能数据读取时,您可能需要与高速缓存行边界对齐,每个高速缓存行通常为64个字节,并且在使用进程间套接字时必须使用它们。 使用Interprocess套接字时,控制变量不能分布在多个高速缓存行或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;

与较慢的方式相反:

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

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

通常在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);

这是更快的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
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM