在C ++ 11中对齐内存的推荐方法是什么


67

我正在研究单生产者单消费者环形缓冲区实现,我有两个要求:

  1. 将单个分配的环形缓冲区实例与缓存行对齐。
  2. 将环形缓冲区中的字段与高速缓存行对齐(以防止错误共享)。

我的课看起来像这样:

#define CACHE_LINE_SIZE 64  // To be used later.

template<typename T, uint64_t num_events>
class RingBuffer {  // This needs to be aligned to a cache line.
public:
  ....

private:
  std::atomic<int64_t> publisher_sequence_ ;
  int64_t cached_consumer_sequence_;
  T* events_;
  std::atomic<int64_t> consumer_sequence_;  // This needs to be aligned to a cache line.

};

让我首先解决点1,即对齐类的单个堆分配实例。有几种方法:

  1. 使用c ++ 11alignas(..)说明符:

    template<typename T, uint64_t num_events>
    class alignas(CACHE_LINE_SIZE) RingBuffer {
    public:
      ....
    
    private:
      // All the private fields.
    
    };
    
  2. 使用posix_memalign(..)+放置new(..)而不更改类定义。这受制于与平台无关的问题:

    void* buffer;
    if (posix_memalign(&buffer, 64, sizeof(processor::RingBuffer<int, kRingBufferSize>)) != 0) {
        perror("posix_memalign did not work!");
        abort();
    }
    // Use placement new on a cache aligned buffer.
    auto ring_buffer = new(buffer) processor::RingBuffer<int, kRingBufferSize>();
    
  3. 使用GCC / Clang扩展 __attribute__ ((aligned(#)))

    template<typename T, uint64_t num_events>
    class RingBuffer {
    public:
      ....
    
    private:
      // All the private fields.
    
    } __attribute__ ((aligned(CACHE_LINE_SIZE)));
    
  4. 我尝试使用C ++ 11标准化aligned_alloc(..)函数代替,posix_memalign(..)但是在Ubuntu 12.04上的GCC 4.8.1在以下位置找不到定义stdlib.h

是否所有这些都可以做同一件事?我的目标是高速缓存行对齐,因此任何对对齐有限制(例如双字)的方法都不会这样做。alignas(..)次要目标是使用标准的平台独立性。

我不清楚是否alignas(..)__attribute__((aligned(#)))有一定的限额,这可能是本机上的高速缓存线以下。我再也无法重现此信息,但是在打印地址时,我想我并不总是使用来获得64字节对齐的地址alignas(..)。相反,posix_memalign(..)似乎总是可行。我再也不能重现这个,所以也许我是在犯错误。

第二个目标是将类/结构中的字段与高速缓存行对齐。我这样做是为了防止虚假共享。我尝试了以下方法:

  1. 使用C ++ 11alignas(..)说明符:

    template<typename T, uint64_t num_events>
    class RingBuffer {  // This needs to be aligned to a cache line.
      public:
      ...
      private:
        std::atomic<int64_t> publisher_sequence_ ;
        int64_t cached_consumer_sequence_;
        T* events_;
        std::atomic<int64_t> consumer_sequence_ alignas(CACHE_LINE_SIZE);
    };
    
  2. 使用GCC / Clang扩展 __attribute__ ((aligned(#)))

    template<typename T, uint64_t num_events>
    class RingBuffer {  // This needs to be aligned to a cache line.
      public:
      ...
      private:
        std::atomic<int64_t> publisher_sequence_ ;
        int64_t cached_consumer_sequence_;
        T* events_;
        std::atomic<int64_t> consumer_sequence_ __attribute__ ((aligned (CACHE_LINE_SIZE)));
    };
    

这两种方法似乎都对齐consumer_sequence到对象开始之后64字节的地址,因此consumer_sequence缓存是否对齐取决于对象本身是否缓存对齐。我的问题是-是否有更好的方法可以做到这一点?

编辑:

原因aligned_alloc在我的计算机上不起作用是因为我使用的是eglibc 2.15(Ubuntu 12.04)。它适用于更高版本的eglibc。

手册页中该功能aligned_alloc()已添加到版本2.16的glibc中

这对我来说毫无用处,因为我不能使用eglibc / glibc这样的最新版本。


5
很好的问题,请参阅Michael Spencer的BoostCon 2013演讲。我不认为您可以移植到超过16个字节的位置(因此,标准不支持64字节的缓存行以及与虚拟内存页面的更大对齐)。
TemplateRex

@TemplateRex感谢您的链接。谈话似乎有关+ 1
拉吉夫·

Answers:


31

不幸的是,我发现最好的方法是分配额外的空间,然后使用“对齐”部分。因此,RingBuffernew可以请求额外的64个字节,然后返回其中的前64个字节对齐的部分。这会浪费空间,但会提供您所需的对齐方式。您可能需要先设置内存,然后再将其返回到实际的分配地址才能取消分配。

[Memory returned][ptr to start of memory][aligned memory][extra memory]

(假设没有从RingBuffer继承)

void * RingBuffer::operator new(size_t request)
{
     static const size_t ptr_alloc = sizeof(void *);
     static const size_t align_size = 64;
     static const size_t request_size = sizeof(RingBuffer)+align_size;
     static const size_t needed = ptr_alloc+request_size;

     void * alloc = ::operator new(needed);
     void *ptr = std::align(align_size, sizeof(RingBuffer),
                          alloc+ptr_alloc, request_size);

     ((void **)ptr)[-1] = alloc; // save for delete calls to use
     return ptr;  
}

void RingBuffer::operator delete(void * ptr)
{
    if (ptr) // 0 is valid, but a noop, so prevent passing negative memory
    {
           void * alloc = ((void **)ptr)[-1];
           ::operator delete (alloc);
    }
}

对于第二个要求RingBuffer也要对齐64字节的数据成员的要求,为此,如果您知道的开头this是对齐的,则可以填充以强制对数据成员进行对齐。


显然,这似乎是一种更标准的方法,但需要注意的是,标准不要求任何超过16个字节的对齐请求。我会接受它,因为这似乎比我的posix_memalign(..)解决方案更可移植。
拉吉夫2013年

1
alloc使用的储蓄delete应该使用void*,不是吗?
Ben Voigt 2013年

1
“(((void **)ptr)[-1] =分配;” -这个编译器不依赖吗?
Stefan Monov

@StefanMonov我不确定为什么它是编译器相关的ptr,至少指向sizeof(void *)过去的字节allocptr[-1]仍然应该> =alloc
Glenn Teitelbaum

@GlennTeitelbaum:啊,我不好,抱歉:)
Stefan Monov


4

经过更多研究后,我的想法是:

  1. 就像@TemplateRex指出的那样,似乎没有一种标准的对齐方式来对齐16个以上的字节。因此,即使我们使用标准格式alignas(..),也不能保证除非对齐边界小于或等于16个字节。我必须验证它是否可以在目标平台上正常工作。

  2. __attribute ((aligned(#)))alignas(..)无法像我怀疑的那样用于对齐分配给堆的对象,即new()对这些注释不做任何事情。它们似乎适用于静态对象或具有(1)中警告的堆栈分配。

    无论是posix_memalign(..)(非标)或aligned_alloc(..)(标准化,但实在提不起工作的GCC 4.8.1)+位置new(..)似乎是解决方案。当我需要平台独立代码时,我的解决方案是特定于编译器的宏:)

  3. struct / class字段的对齐似乎可以同时使用__attribute ((aligned(#)))alignas()正如答案中所述。再次,我认为(1)中关于对齐保证的警告。

所以我当前的解决方案是使用 posix_memalign(..)+放置new(..)来对齐类的堆分配实例,因为我现在的目标平台仅是Linux。我也alignas(..)用于对齐字段,因为它是标准化的,至少在Clang和GCC上有效。如果有更好的答案,我将很乐意进行更改。


在实践中alignas(64)甚至更高的水平上都能奏效。
彼得·科德斯

2

我不知道这是否是对齐分配给新运算符的内存的最佳方法,但这当然非常简单!

这是在GCC 6.1.0中的线程清除程序传递中完成的方式

#define ALIGNED(x) __attribute__((aligned(x)))

static char myarray[sizeof(myClass)] ALIGNED(64) ;
var = new(myarray) myClass;

好吧,在sanitizer_common / sanitizer_internal_defs.h中,它也是这样写的

// Please only use the ALIGNED macro before the type.
// Using ALIGNED after the variable declaration is not portable!        

所以我不知道为什么在变量声明后使用ALIGNED。但这是另一个故事。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.