¿Cómo resolver el problema de alineación de 32 bytes para las operaciones de carga / almacenamiento de AVX?

Estoy teniendo un problema de alineación al usar registros ymm , con algunos fragmentos de código que me parecen bien. Aquí hay un ejemplo de trabajo mínimo:

 #include  #include  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; } 

Ciertamente, sizeof(float) es 4 en mi architecture ( Intel (R) Xeon (R) CPU E5-2650 v2 @ 2.60GHz ) y estoy comstackndo con gcc usando -O3 -march=native flags -O3 -march=native . Por supuesto, el error desaparece con el acceso de memoria no alineado, es decir, especificando _mm256_storeu_ps . Tampoco tengo este problema en los registros xmm , es decir,

 inline void ones_sse(float *a) { __m128 out_aligned = _mm_set1_ps(1.0f); _mm_store_ps(a,out_aligned); } 

¿Estoy haciendo algo tonto? ¿Cuál es la solución para esto?

Los asignadores estándar probablemente solo se alineen con 8B (el ancho del tipo estándar más ancho), o quizás 16B si el tipo más ancho tiene ese requisito (por ejemplo, long double en algunos ABI x86-64).

Opciones:

  • std::aligned_alloc : ISO C ++ 17. principal inconveniente: el tamaño debe ser un múltiplo de alineación . Este requisito de muerte cerebral hace que sea inadecuado para asignar una matriz alineada con la línea de caché de 64B de un número desconocido de float , por ejemplo. O especialmente una matriz alineada con 2M para aprovechar las enormes páginas transparentes .

    La versión C de aligned_alloc se agregó en ISO C11. Está disponible en algunos, pero no en todos los comstackdores de C ++. Como se indica en la página cppreference, no se requiere que la versión C11 falle cuando el tamaño no es un múltiplo de la alineación (es un comportamiento indefinido), por lo que muchas implementaciones proporcionaron el comportamiento obvio deseado como una “extensión”. La discusión está en curso para solucionar esto , pero por ahora realmente no puedo recomendar aligned_alloc como una forma portátil de asignar matrices de tamaño arbitrario.

    Además, los comentaristas informan que no está disponible en MSVC ++. Vea el mejor método multiplataforma para obtener memoria alineada para un #ifdef viable para Windows. Pero AFAIK no tiene funciones de asignación alineadas con Windows que produzcan punteros compatibles con la versión estándar free .

  • posix_memalign : Parte de POSIX 2001, no cualquier estándar ISO C o C ++. Clunky prototipo / interfaz en comparación con aligned_alloc . He visto que gcc genera recargas del puntero porque no estaba seguro de que las reservas en el búfer no modificaran el puntero. (Debido a que posix_memalign se pasa la dirección del puntero). Si usa esto, copie el puntero en otra variable de C ++ a la que no se le haya pasado su dirección fuera de la función.

 #include  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 : Disponible en cualquier plataforma donde _mm_whatever_ps esté disponible, pero no puede pasar los punteros desde él a la _mm_whatever_ps free . En muchas implementaciones C y C ++, _mm_free y free son compatibles, pero no se garantiza que sean portátiles. (Y a diferencia de los otros dos, fallará en tiempo de ejecución, no de comstackción.) En MSVC en Windows, _mm_malloc usa _aligned_malloc , que no es compatible con free ; se bloquea en la práctica.

  • En C ++ 11 y posterior: use alignas(32) float avx_array[1234] como el primer miembro de un miembro struct / class (o en una matriz simple directamente) para que los objetos de almacenamiento estáticos y automáticos de ese tipo tengan una alineación 32B. std::aligned_storage documentación de std::aligned_storage tiene un ejemplo de esta técnica para explicar lo que hace std::aligned_storage .

    En realidad, esto no funciona para el almacenamiento asignado dinámicamente (como un std::vector ), consulte Hacer que std :: vector asigne la memoria alineada .

    En C ++ 17, las alignas finalmente serán utilizables para la asignación dinámica alineada.


Y finalmente, la última opción es tan mala que ni siquiera es parte de la lista: asigna un buffer más grande y agrega do p+=31; p&=~31ULL p+=31; p&=~31ULL con casting apropiado. Demasiados inconvenientes (difícil de liberar, desperdicia memoria) que valdría la pena discutir, ya que las funciones de asignación alineadas están disponibles en todas las plataformas que admiten las _mm256 intrínsecas de Intel _mm256 . Pero incluso hay funciones de biblioteca que te ayudarán a hacer esto, IIRC.

El requisito de usar _mm_free lugar de free probablemente exista para la posibilidad de implementar _mm_malloc encima de un malloc simple usando esta técnica.

Hay dos aspectos intrínsecos para la gestión de la memoria. _mm_malloc funciona como un malloc estándar, pero toma un parámetro adicional que especifica la alineación deseada. En este caso, una alineación de 32 bytes. Cuando se usa este método de asignación, la memoria debe liberarse con la correspondiente llamada _mm_free.

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

Necesitará asignadores alineados.

Pero no hay una razón por la que no puedas agruparlos:

 template 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* = nullptr > aligned_free( aligned_free ) {} }; template struct aligned_free:aligned_free{}; template using mm_ptr = std::unique_ptr< T, aligned_free >; template struct aligned_make; template struct aligned_make { mm_ptr operator()(size_t N)const { return mm_ptr(static_cast(_mm_malloc(sizeof(T)*N, align))); } }; template struct aligned_make { mm_ptr operator()()const { return aligned_make{}(1); } }; template struct aligned_make { mm_ptr operator()()const { return aligned_make{}(N); } }: // T[N] and T versions: template auto make_aligned() -> std::result_of_t()> { return aligned_make{}(); } // T[] version: template auto make_aligned(size_t N) -> std::result_of_t(size_t)> { return aligned_make{}(N); } 

ahora mm_ptr es un puntero único para una matriz de float que está alineado con 4 bytes. Se crea a través de make_aligned(20) , que crea 20 flotantes alineados de 4 bytes, o make_aligned() (constante de tiempo de comstackción solo en esa syntax). make_aligned devuelve mm_ptr no mm_ptr .

Un mm_ptr puede mover-construir un mm_ptr pero no viceversa, lo cual creo que es bueno.

mm_ptr puede tomar cualquier alineación, pero no garantiza ninguna.

Overhead, como con std::unique_ptr , es básicamente cero por puntero. La sobrecarga del código se puede minimizar mediante una inline agresiva.