Как выровнены векторные данные?
Если я хочу обработать данные в std::vector
с SSE мне нужно 16 байтов. Как я могу этого достичь? Нужно ли мне писать свой собственный распределитель? Или распределитель по умолчанию уже выравнивается по 16-байтовым границам?
9 ответов
Стандарт C++ требует выделения функций (malloc()
а также operator new()
) для выделения памяти соответствующим образом выровненной для любого стандартного типа. Поскольку эти функции не получают требование выравнивания в качестве аргумента, на практике это означает, что выравнивание для всех распределений является одинаковым и является выравниванием стандартного типа с наибольшим требованием выравнивания, которое часто long double
и / или long long
(см. boost max_align union).
Векторные инструкции, такие как SSE и AVX, имеют более строгие требования выравнивания (16-байтовое выравнивание для 128-битного доступа и 32-байтовое выравнивание для 256-битного доступа), чем те, которые предусмотрены стандартными функциями выделения C++. posix_memalign()
или же memalign()
может использоваться для удовлетворения таких распределений с более строгими требованиями выравнивания.
Вы должны использовать пользовательский распределитель с std::
контейнеры, такие как vector
, Не могу вспомнить, кто написал следующий, но я использовал его в течение некоторого времени, и это, кажется, работает (возможно, вам придется изменить _aligned_malloc
в _mm_malloc
в зависимости от компилятора / платформы):
#ifndef ALIGNMENT_ALLOCATOR_H
#define ALIGNMENT_ALLOCATOR_H
#include <stdlib.h>
#include <malloc.h>
template <typename T, std::size_t N = 16>
class AlignmentAllocator {
public:
typedef T value_type;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
typedef T * pointer;
typedef const T * const_pointer;
typedef T & reference;
typedef const T & const_reference;
public:
inline AlignmentAllocator () throw () { }
template <typename T2>
inline AlignmentAllocator (const AlignmentAllocator<T2, N> &) throw () { }
inline ~AlignmentAllocator () throw () { }
inline pointer adress (reference r) {
return &r;
}
inline const_pointer adress (const_reference r) const {
return &r;
}
inline pointer allocate (size_type n) {
return (pointer)_aligned_malloc(n*sizeof(value_type), N);
}
inline void deallocate (pointer p, size_type) {
_aligned_free (p);
}
inline void construct (pointer p, const value_type & wert) {
new (p) value_type (wert);
}
inline void destroy (pointer p) {
p->~value_type ();
}
inline size_type max_size () const throw () {
return size_type (-1) / sizeof (value_type);
}
template <typename T2>
struct rebind {
typedef AlignmentAllocator<T2, N> other;
};
bool operator!=(const AlignmentAllocator<T,N>& other) const {
return !(*this == other);
}
// Returns true if and only if storage allocated from *this
// can be deallocated from other, and vice versa.
// Always returns true for stateless allocators.
bool operator==(const AlignmentAllocator<T,N>& other) const {
return true;
}
};
#endif
Используйте это так (замените 16 на другое выравнивание, если необходимо):
std::vector<T, AlignmentAllocator<T, 16> > bla;
Это, однако, только гарантирует, что блок памяти std::vector
использует выравнивание по 16 байтов. Если sizeof(T)
не кратно 16, некоторые ваши элементы не будут выровнены. В зависимости от вашего типа данных, это может быть не проблема. Если T
является int
(4 байта), загружать только элементы, индекс которых кратен 4. Если это double
(8 байт), только кратные 2 и т. Д.
Реальная проблема заключается в том, если вы используете классы как T
, в этом случае вам нужно будет указать требования к выравниванию в самом классе (опять же, в зависимости от компилятора, это может отличаться; пример для GCC):
class __attribute__ ((aligned (16))) Foo {
__attribute__ ((aligned (16))) double u[2];
};
Мы почти закончили! Если вы используете Visual C++ (по крайней мере, версия 2010), вы не сможете использовать std::vector
с классами, выравнивание которых вы указали, из-за std::vector::resize
,
При компиляции, если вы получите следующую ошибку:
C:\Program Files\Microsoft Visual Studio 10.0\VC\include\vector(870):
error C2719: '_Val': formal parameter with __declspec(align('16')) won't be aligned
Вам придется взломать свой stl::vector header
файл:
- Найдите
vector
заголовочный файл [C:\Program Files\Microsoft Visual Studio 10.0\VC\include\vector] - Найдите
void resize( _Ty _Val )
метод [строка 870 на VC2010] - Измените это на
void resize( const _Ty& _Val )
,
Вместо того, чтобы писать свой собственный распределитель, как предлагалось ранее, вы можете использовать boost::alignment::aligned_allocator
за std::vector
как это:
#include <vector>
#include <boost/align/aligned_allocator.hpp>
template <typename T>
using aligned_vector = std::vector<T, boost::alignment::aligned_allocator<T, 16>>;
Короткий ответ:
Если sizeof(T)*vector.size() > 16
тогда да.
Предполагая, что ваш вектор использует нормальные распределители
Предостережение: пока alignof(std::max_align_t) >= 16
так как это максимальное выравнивание.
Длинный ответ:
Обновлено 25 августа 2017 года новый стандарт N4659
Если он выровнен для чего-либо, что больше 16, он также выровнен правильно для 16.
6.11 Выравнивание (пункт 4/5)
Выравнивания представлены в виде значений типа std::size_t. Допустимые выравнивания включают только те значения, которые возвращаются выражением alignof для основных типов, плюс дополнительный набор значений, определенный реализацией, который может быть пустым. Каждое значение выравнивания должно быть неотрицательной интегральной степенью двойки.
Выравнивания имеют порядок от более слабого к более сильному или более строгому выравниванию. Более строгие выравнивания имеют большие значения выравнивания. Адрес, который удовлетворяет требованию выравнивания, также удовлетворяет любому более слабому действительному требованию выравнивания.
new и new[] возвращают значения, которые выровнены так, что объекты правильно выровнены по их размеру:
8.3.4 Новый (пункт 17)
[Примечание: когда функция выделения возвращает значение, отличное от нуля, это должен быть указатель на блок хранения, в котором зарезервировано пространство для объекта. Предполагается, что блок хранения выровнен соответствующим образом и имеет запрошенный размер. Адрес созданного объекта не обязательно будет таким же, как адрес блока, если объект является массивом. - конец примечания]
Обратите внимание, что большинство систем имеют максимальное выравнивание. Динамически распределенная память не должна быть выровнена по значению, большему, чем это.
6.11 Выравнивание (пункт 2)
Фундаментальное выравнивание представлено выравниванием, меньшим или равным наибольшему выравниванию, поддерживаемому реализацией во всех контекстах, которое равно alignof(std::max_align_t) (21.2). Выравнивание, требуемое для типа, может отличаться, когда он используется как тип законченного объекта и когда он используется как тип подобъекта.
Таким образом, если ваша выделенная векторная память больше 16 байтов, она будет правильно выровнена по границам 16 байтов.
Напишите свой собственный распределитель. allocate
а также deallocate
являются важными. Вот один пример:
pointer allocate( size_type size, const void * pBuff = 0 )
{
char * p;
int difference;
if( size > ( INT_MAX - 16 ) )
return NULL;
p = (char*)malloc( size + 16 );
if( !p )
return NULL;
difference = ( (-(int)p - 1 ) & 15 ) + 1;
p += difference;
p[ -1 ] = (char)difference;
return (T*)p;
}
void deallocate( pointer p, size_type num )
{
char * pBuffer = (char*)p;
free( (void*)(((char*)p) - pBuffer[ -1 ] ) );
}
Современный ответ на устаревший (но важный) вопрос.
Написание собственного
Allocator
класс [шаблон] сразу приходит на ум, как говорят другие. Начиная с С++ 11 и до С++ 17 реализация будет в основном ограничена (по стандарту) использованием
alignas
и размещение
new
. C++17 поднимает C11
aligned_alloc
что удобно. Кроме того, C++17
std::pmr
пространство имен (заголовок
<memory_resource>
) представляет
polymorphic_allocator
шаблон класса и
memory_resource
абстрактный интерфейс для полиморфных распределений, сильно вдохновленный Boost. Было показано, что помимо возможности использования действительно универсального динамического кода, в некоторых случаях они обеспечивают повышение скорости; в этом случае ваш SIMD-код будет работать еще лучше.
Использование declspec(align(x,y))
как описано в руководстве по векторизации для Intel, http://d3f8ykwhia686p.cloudfront.net/1live/intel/CompilerAutovectorizationGuide.pdf
Стандарт обязывает new
а также new[]
вернуть данные, выровненные для любого типа данных, который должен включать SSE. Соблюдает ли MSVC это правило или нет - это другой вопрос.
Не думайте о контейнерах STL. Их интерфейс / поведение определяется, но не то, что стоит за ними. Если вам нужен необработанный доступ, вы должны написать свою собственную реализацию, которая соответствует правилам, которые вы хотели бы иметь.