Как решить проблему 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> это уникальный указатель на массив floats это 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, в основном ноль на указатель. Накладные расходы кода могут быть минимизированы агрессивным inlineING.

Другие вопросы по тегам