Как решить проблему 32-байтового выравнивания для операций загрузки / хранения AVX?
У меня проблема с выравниванием при использовании ymm
регистры, с некоторыми фрагментами кода, который мне кажется нормальным. Вот минимальный рабочий пример:
#include <iostream>
#include <immintrin.h>
inline void ones(float *a)
{
__m256 out_aligned = _mm256_set1_ps(1.0f);
_mm256_store_ps(a,out_aligned);
}
int main()
{
size_t ss = 8;
float *a = new float[ss];
ones(a);
delete [] a;
std::cout << "All Good!" << std::endl;
return 0;
}
Конечно, sizeof(float)
является 4
на моей архитектуре ( Intel (R) Xeon (R) CPU E5-2650 v2 @ 2.60GHz), и я собираю с gcc
с помощью -O3 -march=native
флаги. Конечно, ошибка исчезает с доступом к памяти без выравнивания, т.е. _mm256_storeu_ps
, У меня тоже нет этой проблемы на xmm
регистры, т.е.
inline void ones_sse(float *a)
{
__m128 out_aligned = _mm_set1_ps(1.0f);
_mm_store_ps(a,out_aligned);
}
Я делаю что-нибудь глупое? какой обходной путь для этого?
3 ответа
Стандартные распределители, вероятно, выравниваются только до 8B (ширина самого широкого стандартного типа) или, возможно, до 16B, если у самого широкого типа есть такое требование (например, long double
в некоторых x86-64 ABI).
Опции:
std::aligned_alloc
: ISO C++17. главный недостаток: размер должен быть кратным выравниванию. Это требование мозговой смерти делает его неуместным для выделения 64-битного выравниваемого массива кэша с неизвестным числомfloat
с, например. Или, особенно, массив размером 2M, чтобы использовать прозрачные огромные страницы.C версия
aligned_alloc
был добавлен в ISO C11. Он доступен в некоторых, но не во всех компиляторах C++. Как отмечалось на странице cppreference, версия C11 не требовала сбоя, когда размер не кратен выравниванию (это неопределенное поведение), поэтому многие реализации предоставили очевидное желаемое поведение в качестве "расширения". Обсуждение ведется, чтобы исправить это, но пока я не могу порекомендоватьaligned_alloc
в качестве переносного способа выделения массивов произвольного размера.Кроме того, комментаторы сообщают, что это недоступно в MSVC++. Смотрите лучший кроссплатформенный метод, чтобы выровнять память для жизнеспособного
#ifdef
для Windows. Но в AFAIK нет функций выравнивания-выравнивания Windows, которые выдают указатели, совместимые со стандартом.free
,posix_memalign
: Часть POSIX 2001, а не какой-либо стандарт ISO C или C++. Неуклюжий прототип / интерфейс по сравнению сaligned_alloc
, Я видел, как gcc генерирует перезагрузки указателя, потому что он не был уверен, что хранилища в буфере не изменили указатель. (Посколькуposix_memalign
передается адрес указателя.) Поэтому, если вы используете это, скопируйте указатель в другую переменную C++, чей адрес не был передан за пределы функции.
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size); // POSIX 2001
void *aligned_alloc(size_t alignment, size_t size); // C11 (and ISO C++17)
_mm_malloc
: Доступно на любой платформе, где_mm_whatever_ps
доступен, но вы не можете передать указатели от него наfree
, На многих реализациях C и C++_mm_free
а такжеfree
совместимы, но не гарантированно будут переносимыми. (И в отличие от двух других, он не будет работать во время выполнения, а не во время компиляции.) На MSVC в Windows_mm_malloc
использования_aligned_malloc
, который не совместим сfree
; это терпит крах на практике.В C++11 и более поздних версиях: используйте
alignas(32) float avx_array[1234]
как первый член члена структуры / класса (или непосредственно в простом массиве), статические и автоматические объекты хранения этого типа будут иметь выравнивание 32B.std::aligned_storage
документация имеет пример этой техники, чтобы объяснить, чтоstd::aligned_storage
делает.Это на самом деле не работает для динамически распределенного хранилища (например,
std::vector<my_class_with_aligned_member_array>
), см. Создание std::vector для выделения выровненной памяти.В C++17
alignas
наконец будет использоваться для выравнивания динамического выделения.
И, наконец, последний вариант настолько плох, что даже не входит в список: выделите больший буфер и добавьте do p+=31; p&=~31ULL
с соответствующим литьем. Слишком много недостатков (трудно освободить, тратить память), которые стоит обсудить, поскольку функции выравнивания-распределения доступны на каждой платформе, поддерживающей Intel _mm256
. встроенные функции Но есть даже библиотечные функции, которые помогут вам сделать это, IIRC.
Требование к использованию _mm_free
вместо free
вероятно существует для возможности реализации _mm_malloc
поверх простого старого malloc
используя эту технику.
Есть две встроенные функции для управления памятью. _mm_malloc работает как стандартный malloc, но принимает дополнительный параметр, который задает желаемое выравнивание. В этом случае выравнивание 32 байта. Когда используется этот метод выделения, память должна быть освобождена с помощью соответствующего вызова _mm_free.
float *a = static_cast<float*>(_mm_malloc(sizeof(float) * ss , 32));
...
_mm_free(a);
Вам понадобятся согласованные распределители.
Но нет причины, по которой вы не можете их связать:
template<class T, size_t align>
struct aligned_free {
void operator()(T* t)const{
ASSERT(!(uint_ptr(t) % align));
_mm_free(t);
}
aligned_free() = default;
aligned_free(aligned_free const&) = default;
aligned_free(aligned_free&&) = default;
// allow assignment from things that are
// more aligned than we are:
template<size_t o,
std::enable_if_t< !(o % align) >* = nullptr
>
aligned_free( aligned_free<T, o> ) {}
};
template<class T>
struct aligned_free<T[]>:aligned_free<T>{};
template<class T, size_t align=1>
using mm_ptr = std::unique_ptr< T, aligned_free<T, align> >;
template<class T, size_t align>
struct aligned_make;
template<class T, size_t align>
struct aligned_make<T[],align> {
mm_ptr<T, align> operator()(size_t N)const {
return mm_ptr<T, align>(static_cast<T*>(_mm_malloc(sizeof(T)*N, align)));
}
};
template<class T, size_t align>
struct aligned_make {
mm_ptr<T, align> operator()()const {
return aligned_make<T[],align>{}(1);
}
};
template<class T, size_t N, size_t align>
struct aligned_make<T[N], align> {
mm_ptr<T, align> operator()()const {
return aligned_make<T[],align>{}(N);
}
}:
// T[N] and T versions:
template<class T, size_t align>
auto make_aligned()
-> std::result_of_t<aligned_make<T,align>()>
{
return aligned_make<T,align>{}();
}
// T[] version:
template<class T, size_t align>
auto make_aligned(size_t N)
-> std::result_of_t<aligned_make<T,align>(size_t)>
{
return aligned_make<T,align>{}(N);
}
сейчас mm_ptr<float[], 4>
это уникальный указатель на массив float
s это 4 байта выровнены. Вы создаете это через make_aligned<float[], 4>(20)
, который создает 20 поплавков с 4-байтовым выравниванием, или make_aligned<float[20], 4>()
(константа времени компиляции только в этом синтаксисе). make_aligned<float[20],4>
возвращается mm_ptr<float[],4>
не mm_ptr<float[20],4>
,
mm_ptr<float[], 8>
может двигаться-построить mm_ptr<float[],4>
но не наоборот, что я считаю хорошим.
mm_ptr<float[]>
может принять любое выравнивание, но не гарантирует ни одного.
Накладные, как с std::unique_ptr
, в основном ноль на указатель. Накладные расходы кода могут быть минимизированы агрессивным inline
ING.