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

时间:2011-12-12 02:50:09

标签: 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_;
    
  5. 这个概念在gcc下是否适用于c代码?

2 个答案:

答案 0 :(得分:41)

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

在您给出的特定示例中,一个核心可以将一个条目入队(读取(共享)buffer_并仅写入{仅限enqueue_pos_),而另一个核心可以排队(共享buffer_和独占dequeue_pos_),没有核心停止在另一个拥有的缓存行上。

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

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

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

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

答案 1 :(得分:3)

在处理中断或高性能数据读取时,您可能需要对齐缓存行边界,通常为每个缓存行64字节,并且在使用进程间套接字时必须使用它们。使用进程间套接字,不能将控制变量分散到多个高速缓存行或DDR RAM字中,否则将导致L1,L2等或高速缓存或DDR RAM充当低通滤波器并滤除中断数据!那很不好!!!这意味着当您的算法很好时,您会得到奇怪的错误,并且有可能使您发疯!

几乎总是以128位字(DDR RAM字)读取DDR RAM,这是16个字节,因此环形缓冲区变量不得分散在多个DDR RAM字之间。某些系统确实使用64位DDR RAM字,从技术上讲,您可以在16位CPU上获得32位DDR RAM字,但在这种情况下将使用SDRAM。

在高性能算法中读取数据时,可能只对减少使用的高速缓存行数感兴趣。就我而言,我开发了世界上最快的整数到字符串算法(比以前的最快算法快40%),并且我正在优化Grisu算法,这是世界上最快的浮点算法。为了打印浮点数,您必须打印整数,因此为了优化Grisu,我实现的一项优化是将Grisu的查找表(LUT)缓存行对齐到正好15个缓存行,即它实际上是那样排列的,这很奇怪。这将从.bss节(即静态内存)中获取LUT,并将其放置到堆栈(或堆中,但Stack更合适)。我没有对此进行基准测试,但是提出它很好,并且我学到了很多,最快的加载值方法是从i缓存而不是d缓存加载它们。区别在于,i缓存是只读的,并且具有更大的缓存行,因为它是只读的(2KB是一位教授引用我的内容。)。因此,实际上您将破坏数组索引的性能,而不是像这样加载变量:

int faster_way = 12345678;

与较慢的方式相反:

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

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

通常在C ++中,您将使用std::align函数。我建议不要使用此功能,因为it is not guaranteed to work optimally。这是对齐高速缓存行的最快方法,首先,我是作者,这是一个无礼的插件:

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
}