Каков рекомендуемый способ выравнивания памяти на С++ 11

Я работаю над единственной реализацией одного кольцевого буфера для одного производителя. У меня есть два требования:

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) Используйте спецификатор С++ 11 alignas(..):

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) Я попытался использовать стандартную С++ 11 функцию aligned_alloc(..) вместо posix_memalign(..), но GCC 4.8.1 на Ubuntu 12.04 не смог найти определение в stdlib.h

Все ли они гарантированно выполняют одно и то же? Моя цель - выравнивание строки в кешках, поэтому любой метод, который имеет некоторые ограничения на выравнивание (например, двойное слово), не будет делать. Независимость от платформы, которая указывает на использование стандартизованного alignas(..), является вторичной целью.

Неясно, есть ли у alignas(..) и __attribute__((aligned(#))) некоторый предел, который может быть ниже строки кэша на машине. Я не могу воспроизвести это больше, но при печати адресов я думаю, что я не всегда получал согласованные адреса по 24 байта с помощью alignas(..). Наоборот, posix_memalign(..), казалось, всегда работал. Снова я не могу воспроизвести это больше, поэтому, возможно, я ошибся.

Вторая цель - выровнять поле внутри класса/структуры в строке кэша. Я делаю это для предотвращения ложного обмена. Я пробовал следующие способы:

1) Используйте спецификатор С++ 11 alignas(..):

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 выравнивается по кешу, зависит от того, был ли сам объект привязан к кешу. Здесь мой вопрос: есть ли лучшие способы сделать то же самое?

EDIT: Причина, по которой aligned_alloc не работала на моей машине, заключалась в том, что я был на eglibc 2.15 (Ubuntu 12.04). Он работал над более поздней версией eglibc.

На странице man: The function aligned_alloc() was added to glibc in version 2.16.

Это делает его довольно бесполезным для меня, так как я не могу требовать такую ​​последнюю версию eglibc/glibc.

Ответ 1

К сожалению, лучшее, что я нашел, это выделить дополнительное пространство, а затем использовать "выровненную" часть. Таким образом, RingBuffer new может запросить дополнительные 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 выровнено, вы можете наложить для выравнивания элементов данных.

Ответ 2

Ответ на вашу проблему - std:: aligned_storage. Его можно использовать на верхнем уровне и для отдельных членов класса.

Ответ 3

После еще нескольких исследований мои мысли:

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(..) + placement new(..) для выравнивания выделенного кучи экземпляра моего класса, так как моя целевая платформа прямо сейчас - только Linux. Я также использую alignas(..) для выравнивания полей, поскольку он стандартизован и, по крайней мере, работает на Clang и GCC. Я буду рад изменить его, если появится лучший ответ.

Ответ 4

Я не знаю, является ли это наилучшим способом выравнивания памяти, выделенной с помощью нового оператора, но это, безусловно, очень просто!

Так делается в проходе дезинфицирующего средства в 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 здесь используется после объявления переменной. Но это другая история.