¿Es `reinterpret_cast`ing entre el puntero vectorial SIMD de hardware y el tipo correspondiente un comportamiento indefinido?
¿ Es legal acceder reinterpret_cast
a 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 hwvec1
y arr1
dependen de undefined behavior
s?
¿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
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 __m256i
y 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
/ movq
load/store y aliasing de intrínsecos seguros como _mm_loadu_si32(void*)
. Anteriormente, creo que Intel asumió que usaría _mm_cvtsi32_si128
lo que requería cargarlo int
usted mismo de forma segura, lo que significaba usarlo memcpy
para 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 __m256
y 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 __m256
con un may_alias
atributo: 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
. storeu
arroja el puntero arg al _u
tipo definido con aligned(1)
).
A los vectores nativos de GNU C sin may_alias
se 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_alias
hace 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_ps
para 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 __m256i
accesos fueran vía __m256i*
. Debido a que es un may_alias
tipo, 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
/ __m256i
como un vector de 2 o 4 long long
elementos, y __m128
/ __m256
como un vector de 4 u 8 float
elementos, etc. Manual GCC para Extensiones de Vectores . Estos pueden contar como reales long long
u objetos a los float
que 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 memcpy
4 bytes en un archivo int32_t
. Puede y debe usarlo memcpy
para 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 char
objetos. Entonces , quizás algunos char
miembros de una estructura serían un mejor ejemplo.
Para insertar/extraer elementos vectoriales, utilice intrínsecos aleatorios, SSE2 _mm_insert_epi16
/ _mm_extract_epi16
o 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 + v2
funcione con el tamaño correcto. (La sintaxis vectorial nativa de GNU C hace eso para vectores flotantes/dobles y se define __m128i
como un vector de int64_t con signo, pero MSVC no proporciona operadores en los __m128
tipos 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 __m128
es 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.