¿Es `reinterpret_cast`ing entre el puntero vectorial SIMD de hardware y el tipo correspondiente un comportamiento indefinido?

Resuelto sandthorn asked hace 6 años • 2 respuestas

¿ Es legal acceder reinterpret_casta float*objetos a través de un tipo de puntero diferente?__m256*float

constexpr size_t _m256_float_step_sz = sizeof(__m256) / sizeof(float);
alignas(__m256) float stack_store[100 * _m256_float_step_sz ]{};
__m256& hwvec1 = *reinterpret_cast<__m256*>(&stack_store[0 * _m256_float_step_sz]);

using arr_t = float[_m256_float_step_sz];
arr_t& arr1 = *reinterpret_cast<float(*)[_m256_float_step_sz]>(&hwvec1);

¿ Dependen hwvec1y arr1dependen de undefined behaviors?

¿Violan estrictas reglas de alias? [val.básico]/11

O sólo hay una forma definida de intrínseco:

__m256 hwvec2 = _mm256_load_ps(&stack_store[0 * _m256_float_step_sz]);
_mm256_store_ps(&stack_store[1 * _m256_float_step_sz], hwvec2);

rayo divino

sandthorn avatar Aug 31 '18 16:08 sandthorn
Aceptado

ISO C++ no define __m256, por lo que debemos observar qué define su comportamiento en las implementaciones que los admiten.

Los elementos intrínsecos de Intel definen los punteros vectoriales como __m256*si se les permitiera asignar un alias a cualquier otra cosa, de la misma manera que ISO C++ define char*que se les permite asignar un alias. (Pero no al revés: es UB y se interrumpe en la práctica para apuntar int*con a __m256iy eliminarlo).

Entonces sí, es seguro desreferenciar a __m256*en lugar de usar una _mm256_load_ps()carga intrínseca alineada.

Pero especialmente para float/double, a menudo es más fácil usar los intrínsecos porque float*también se encargan de lanzar desde . Para los números enteros, los intrínsecos de carga/almacenamiento de AVX512 se definen como take void*, pero AVX2 y versiones anteriores necesitan una conversión como (__m256i*)&arr[i]la cual tiene un diseño de API bastante complicado y congestiona el código al usarlo.

También se han agregado algunos intrínsecos que no son AVX512 utilizando la alineación void*like movd/ movqload/store y aliasing de intrínsecos seguros como _mm_loadu_si32(void*). Anteriormente, creo que Intel asumió que usaría _mm_cvtsi32_si128lo que requería cargarlo intusted mismo de forma segura, lo que significaba usarlo memcpypara evitar UB (al menos en compiladores distintos de los clásicos ICC y MSVC, si permiten no estar alineados int*y no imponen un alias estricto). Esto podría haber sido más o menos cuando Intel comenzó a considerar la migración a LLVM para ICX/ICPX/OneAPI y se dio cuenta de lo complicado que era lidiar con cargas limitadas en compiladores que imponen un alias estricto.


Para obtener más información sobre lo que requería la API intrínseca, podemos consultar los detalles de implementación no portátil de GCC. Es de suponer que dedujeron de los ejemplos o la documentación de Intel qué comportamiento era necesario, o es posible que algunos ingenieros de Intel hayan enviado parches. (No debe confiar en los detalles de GCC porque no está documentado y otros compiladores que implementan __m256y otros tipos intrínsecos podrían hacer las cosas de manera diferente. Pero podemos ver que GCC tuvo que permitir explícitamente alias que de otro modo no serían seguros).

En GCC, esto se implementa definiendo __m256con un may_aliasatributo: de gcc7.3 avxintrin.h(uno de los encabezados que <immintrin.h>incluye):

/* The Intel API is flexible enough that we must allow aliasing with other
   vector types, and their scalar components.  */
typedef float __m256 __attribute__ ((__vector_size__ (32),
                                     __may_alias__));
typedef long long __m256i __attribute__ ((__vector_size__ (32),
                                          __may_alias__));
typedef double __m256d __attribute__ ((__vector_size__ (32),
                                       __may_alias__));

/* Unaligned version of the same types.  */
typedef float __m256_u __attribute__ ((__vector_size__ (32),
                                       __may_alias__,
                                       __aligned__ (1)));
typedef long long __m256i_u __attribute__ ((__vector_size__ (32),
                                            __may_alias__,
                                            __aligned__ (1)));
typedef double __m256d_u __attribute__ ((__vector_size__ (32),
                                         __may_alias__,
                                         __aligned__ (1)));

(En caso de que se lo pregunte, es por eso que eliminar la referencia a a __m256*es como _mm256_store_ps, no storeu. storeuarroja el puntero arg al _utipo definido con aligned(1)).

A los vectores nativos de GNU C sin may_aliasse les permite asignar un alias a su tipo escalar, por ejemplo, incluso sin may_alias, se puede convertir con seguridad entre float*y un tipo hipotético v8sf. Pero may_aliashace que sea seguro cargar desde una variedad de int[], char[]o lo que sea.


Otros comportamientos que los intrínsecos de Intel requieren definir

El uso de la API de Intel _mm_storeu_si128( (__m128i*)&arr[i], vec);requiere que usted cree punteros potencialmente no alineados que fallarían si los ignorara. Y _mm_storeu_pspara una ubicación que no está alineada con 4 bytes es necesario crear un archivo float*.

Simplemente crear punteros no alineados, o punteros fuera de un objeto, es UB en ISO C++, incluso si no los elimina. Supongo que esto permite implementaciones en hardware exótico que realizan algunos tipos de comprobaciones de los punteros al crearlos (posiblemente en lugar de al desreferenciarlos), o tal vez que no pueden almacenar los bits bajos de los punteros. (No tengo idea si existe algún hardware específico donde sea posible un código más eficiente gracias a esta UB).

Pero las implementaciones que soportan los elementos intrínsecos de Intel deben definir el comportamiento, al menos para los __m*tipos y float*/ double*. Esto es trivial para los compiladores dirigidos a cualquier CPU moderna normal, incluido x86 con un modelo de memoria plana (sin segmentación); Los punteros en asm son solo números enteros que se mantienen en los mismos registros que los datos. (m68k tiene registros de dirección versus registros de datos, pero nunca falla al mantener patrones de bits que no son direcciones válidas en los registros A, siempre y cuando no los elimine).


Yendo al revés: acceso a elementos de un vector.

Tenga en cuenta que may_alias, al igual que la char*regla de alias, solo funciona en un sentido : no se garantiza que sea seguro usarlo int32_t*para leer un archivo __m256. Puede que ni siquiera sea seguro usarlo float*para leer un archivo __m256. Al igual que no es seguro hacerlo char buf[1024]; int *p = (int*)buf;.

Consulte GCC AVX __m256i cast to int array lleva a valores incorrectos para ver un ejemplo del mundo real de código de ruptura GCC que apunta int*a un __m256i vec;objeto. No es una desreferenciación__m256i* ; eso sería seguro si los únicos __m256iaccesos fueran vía __m256i*. Debido a que es un may_aliastipo, el compilador no puede inferir que el objeto subyacente es un __m256i; Ese es el punto, y por qué es seguro apuntar a int arr[]o lo que sea.

GCC/clang define __m128i/ __m256icomo un vector de 2 o 4 long longelementos, y __m128/ __m256como un vector de 4 u 8 floatelementos, etc. Manual GCC para Extensiones de Vectores . Estos pueden contar como reales long longu objetos a los floatque puede apuntar con seguridad , pero GCC no lo documenta explícitamente ni siquiera para sus tipos de vectores nativos (pero sí define la indexación). Incluso si lo hicieran, eso sería un detalle de implementación de cómo GCC y Clang definen los tipos de vectores de Intel en términos de vectores GNU o Clang, no documentados ni garantizados como portátiles. Excepto MSVC, que permite que cualquier cosa tenga un alias, como . (Y creo que el ICC clásico también era así, a diferencia del ICX basado en LLVM)long long*float*[]-fno-strict-aliasing

Leer/escribir a través de un char*alias puede cualquier cosa, pero cuando tienes un char objeto , el alias estricto hace que sea UB para leerlo a través de otros tipos. (No estoy seguro de si las principales implementaciones en x86 definen ese comportamiento, pero no es necesario confiar en él porque optimizan memcpy4 bytes en un archivo int32_t. Puede y debe usarlo memcpypara expresar una carga no alineada desde un char[]búfer , porque la vectorización automática con un tipo más amplio puede asumir una alineación de 2 bytes para int16_t*, y generar código que falla si no es así: ¿ Por qué el acceso no alineado a la memoria asignada a mm a veces tiene un error de segmentación en AMD64? )

Puede que A char arr[]no sea una gran analogía porque arr[i]se define en términos de *(arr+i), por lo que en realidad hay una char*deref involucrada en el acceso a la matriz como charobjetos. Entonces , quizás algunos charmiembros de una estructura serían un mejor ejemplo.


Para insertar/extraer elementos vectoriales, utilice intrínsecos aleatorios, SSE2 _mm_insert_epi16/ _mm_extract_epi16o SSE4.1 insert / _mm_extract_epi8/32/64. Para float, no hay intrínsecos de inserción/extracción que debas usar con scalar float.

O almacenar en una matriz y leer la matriz. ( imprime una variable __m128i ). En realidad, esto optimiza las instrucciones de extracción de vectores.

La sintaxis vectorial de GNU C proporciona el []operador para vectores, como __m256 v = ...; v[3] = 1.25;. MSVC define los tipos de vectores como una unión con un .m128_f32[]miembro para acceso por elemento.

Hay bibliotecas contenedoras como la biblioteca de clases de vectores de Agner Fog (ahora con licencia Apache) que proporciona operator[]sobrecargas portátiles para sus tipos de vectores, y operador +/ -/ */ <<y así sucesivamente. Es bastante bueno, especialmente para tipos de números enteros donde tener diferentes tipos para diferentes anchos de elementos hace que v1 + v2funcione con el tamaño correcto. (La sintaxis vectorial nativa de GNU C hace eso para vectores flotantes/dobles y se define __m128icomo un vector de int64_t con signo, pero MSVC no proporciona operadores en los __m128tipos base).


También puede utilizar juegos de palabras de unión entre un vector y una matriz de algún tipo, lo cual es seguro en ISO C99 y en GNU C++, pero no en ISO C++. Creo que también es oficialmente seguro en MSVC, porque creo que la forma en que lo definen __m128es una unión normal.

Sin embargo , no hay garantía de que obtenga código eficiente de cualquiera de estos métodos de acceso a elementos. No utilice bucles internos internos y eche un vistazo al conjunto resultante si el rendimiento es importante.

Peter Cordes avatar Aug 31 '2018 14:08 Peter Cordes