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

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

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

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

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

ИЗМЕНИТЬ:

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

Из справочной страницы: Функция aligned_alloc() была добавлена ​​в glibc в версии 2.16. .

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


person Rajiv    schedule 26.12.2013    source источник
comment
отличный вопрос, см. выступление Майкла Спенсера на BoostCon 2013. Я не думаю, что вы можете переносимо выровнять более 16 байтов (поэтому 64-байтовая строка кэша и даже большее выравнивание по страницам виртуальной памяти не поддерживаются стандартом).   -  person TemplateRex    schedule 27.12.2013
comment
@TemplateRex Спасибо за ссылку. Разговор кажется актуальным + 1.   -  person Rajiv    schedule 27.12.2013


Ответы (4)


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

person Glenn Teitelbaum    schedule 26.12.2013
comment
Это определенно кажется более стандартным способом сделать это, с оговоркой, что любой запрос выравнивания более 16 байтов не требуется стандартом. Я приму это, так как это кажется более переносимым, чем мое решение posix_memalign(..). - person Rajiv; 28.12.2013
comment
Ваша экономия alloc для использования с delete должна использовать void*, не так ли? - person Ben Voigt; 28.12.2013
comment
((void **)ptr)[-1] = alloc; - разве этот компилятор не зависит? - person Stefan Monov; 28.12.2017
comment
@StefanMonov Я не уверен, почему это будет зависеть от компилятора ptr указывает как минимум на sizeof(void *) байта после alloc, ptr[-1] все равно должно быть ›= alloc - person Glenn Teitelbaum; 29.12.2017
comment
@GlennTeitelbaum: Ах, как плохо, извини :) - person Stefan Monov; 30.12.2017
comment
@GlennTeitelbaum Придирки: аргумент ptr (3-й аргумент) std::align() является ссылкой на указатель. Так не передает ли этот код ссылку на временный объект (alloc+ptr_alloc)? то есть должно быть void* ptr = alloc+ptr_alloc; ptr = std::align(align_size, sizeof(RingBuffer, ptr, request_size); - person user673679; 22.02.2018
comment
@user673679 user673679 Как вы думаете, почему в этом случае проблема заключается в ссылке на временный объект? Продолжительность жизни кажется ограниченной вызовом функции. - person Glenn Teitelbaum; 07.03.2018

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

person rubenvb    schedule 28.12.2013
comment
Но он имеет те же ограничения, что и alignas (до С++ 17 до 16 байт/зависит от платформы) - person kwesolowski; 20.10.2015

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

  1. Как отметил @TemplateRex, не существует стандартного способа выравнивания более чем по 16 байтам. Таким образом, даже если мы используем стандартизированный alignas(..), нет никакой гарантии, если только граница выравнивания не меньше или равна 16 байтам. Мне нужно убедиться, что он работает должным образом на целевой платформе.

  2. __attribute ((aligned(#))) или alignas(..) нельзя использовать для выравнивания объекта, выделенного в куче, как я подозревал, т.е. new() ничего не делает с этими аннотациями. Кажется, они работают для статических объектов или выделений стека с оговорками из (1).

    Либо posix_memalign(..) (нестандартный), либо aligned_alloc(..) (стандартизированный, но не смог заставить его работать на GCC 4.8.1) + размещение new(..) кажется решением. Мое решение, когда мне нужен независимый от платформы код, - это макросы, специфичные для компилятора :)

  3. Выравнивание полей структуры/класса работает как с __attribute ((aligned(#))), так и с alignas(), как указано в ответе. Опять же, я думаю, что оговорки из (1) о гарантиях на выравнивание остаются в силе.

Итак, мое текущее решение состоит в том, чтобы использовать posix_memalign(..) + размещение new(..) для выравнивания экземпляра, выделенного в куче, моего класса, поскольку моей целевой платформой сейчас является только Linux. Я также использую alignas(..) для выравнивания полей, так как он стандартизирован и, по крайней мере, работает на Clang и GCC. Я буду рад изменить его, если появится лучший ответ.

person Rajiv    schedule 26.12.2013
comment
На практике работает alignas(64) или даже выше. - person Peter Cordes; 29.07.2020

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

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

person Hugo    schedule 19.07.2017