Каков рекомендуемый способ выравнивания памяти в C++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) Используйте C++ 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) Я пытался использовать стандартизированный C++ 11 aligned_alloc(..)
функция вместо 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
выравнивается ли кеш, зависит от того, выровнен ли сам объект. Вот мой вопрос - есть ли лучшие способы сделать то же самое?
РЕДАКТИРОВАТЬ: причина, по которой align_alloc не работает на моей машине, была то, что я был на eglibc 2.15 (Ubuntu 12.04). Это работало на более поздней версии eglibc.
Со страницы руководства: The function aligned_alloc() was added to glibc in version 2.16
,
Это делает его довольно бесполезным для меня, так как я не могу требовать такой недавней версии eglibc/glibc.
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
выровнен, вы можете дополнить выравнивание для членов данных.
Ответом на вашу проблему является std::align_storage. Может использоваться на высшем уровне и для отдельных членов класса.
После еще нескольких исследований мои мысли:
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. Я буду рад изменить его, если придет лучший ответ.
Я не знаю, является ли это лучшим способом выравнивания памяти, выделенной новым оператором, но это, конечно, очень просто!
Это то, как это делается на этапе очистки потока в 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 здесь используется после объявления переменной. Но это другая история.