Как решить проблему 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.60 ГГц), и я компилирую с помощью 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);
}

Я делаю что-нибудь глупое? для чего это необходимо?

Ответ 1

Стандартные распределители обычно выровнены только с alignof(max_align_t), который часто составляет 16B, например long double в x86-64 System V ABI. Но в некоторых 32-разрядных ABI это только 8B, поэтому его даже недостаточно для динамического выделения выровненных векторов __m128, и вам нужно выйти за рамки простого вызова new или malloc.

Статическое и автоматическое хранение легко: используйте alignas(32) float arr[N];

C++ 17 предоставляет выровненный new для выровненного динамического выделения, совместимого с delete:
float * arr = new (std::align_val_t(32)) float[numSteps];
См. документацию для new/new[] и std::align_val_t

Другие параметры динамического выделения в основном совместимы с malloc/free, а не с new/delete:

  • std::aligned_alloc: ISO C++ 17. основной недостаток: размер должен быть кратен выравниванию. Это требование мозговой смерти делает его неуместным для выделения, например, выровненного массива строки кэша 64B с неизвестным числом float. Или, в частности, выровненный массив 2M, чтобы воспользоваться прозрачными огромными страницами.

    Версия C aligned_alloc была добавлена в ISO C11. Он доступен в некоторых, но не во всех компиляторах C++. Как отмечалось на странице cppreference, версия C11 не должна была терпеть неудачу, когда размер не кратен выравниванию (это неопределенное поведение), поэтому многие реализации предоставили очевидное желаемое поведение как "расширение". В настоящее время ведутся обсуждения, чтобы исправить это, но пока я не могу порекомендовать aligned_alloc в качестве переносимого способа выделения массивов произвольного размера.

    Кроме того, комментаторы сообщают, что он недоступен в MSV C++. Смотрите лучший кроссплатформенный метод для получения выровненной памяти для жизнеспособного #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 может быть способ использовать выровненный новый для std::vector. ТОДО: узнай как.


И, наконец, последний вариант настолько плох, что даже не входит в список: выделите больший буфер и добавьте do p+=31; p&=~31ULL с соответствующим приведением. Слишком много недостатков (трудно освободить, тратить память) стоит обсудить, поскольку функции выравнивания-распределения доступны на каждой платформе, поддерживающей встроенные функции Intel _mm256. Но есть даже библиотечные функции, которые помогут вам сделать это, IIRC.

Требование использовать _mm_free вместо free, вероятно, существует для возможности реализации _mm_malloc поверх простого старого malloc с использованием этой техники.

Ответ 2

Есть два интерфейса для управления памятью.  _mm_malloc работает как стандартный malloc, но для этого требуется дополнительный параметр, который указывает желаемое выравнивание. В этом случае выравнивание по 32 байт. Когда этот метод распределения используется, память должна быть освобождена соответствующим вызовом _mm_free.

float *a = static_cast<float*>(_mm_malloc(sizeof(float) * ss , 32));
...
_mm_free(a);

Ответ 3

Вам понадобятся выровненные распределители.

Но нет причин, по которым вы не можете их расслоить:

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);
}

now mm_ptr<float[], 4> - уникальный указатель на массив из float, который выровнен по 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>.

A mm_ptr<float[], 8> может перемещать-построить a mm_ptr<float[],4>, но не наоборот, что я считаю приятным.

mm_ptr<float[]> может принимать любое выравнивание, но не гарантирует его.

Накладные расходы, например, с std::unique_ptr, в основном равны нулю для каждого указателя. Накладные расходы кода могут быть сведены к минимуму с помощью агрессивного inline ing.