c++ - Carga de 8 caracteres de la memoria en una variable__m256 como flotantes de precisión individuales empaquetados
sse simd (1)
Si está utilizando AVX2, puede usar PMOVZX para extender sus caracteres a cero en enteros de 32 bits en un registro de 256b. A partir de ahí, la conversión a flotación puede ocurrir en el lugar.
; rsi = new_image
VPMOVZXBD ymm0, [rsi] ; or SX to sign-extend (Byte to DWord)
VCVTDQ2PS ymm0, ymm0 ; convert to packed foat
Esta es una buena estrategia incluso si desea hacer esto para múltiples vectores, pero aún mejor podría ser
una carga de difusión de 128 bits para alimentar
vpmovzxbd ymm,xmm
y
vpshufb ymm
(
_mm256_shuffle_epi8
) para los 64 bits altos,
porque la familia Intel SnB Las CPU no fusionan micro
vpmovzx ymm,mem
, solo solo
vpmovzx xmm,mem
.
(
https://agner.org/optimize/
).
Las cargas de difusión son uop individuales sin necesidad de puerto ALU, y se ejecutan exclusivamente en un puerto de carga.
Esto es 3 uops totales para bcast-load + vpmovzx + vpshufb.
(TODO: escriba una versión intrínseca de eso. También evita el problema de las optimizaciones perdidas para
_mm_loadl_epi64
->
_mm256_cvtepu8_epi32
).
Por supuesto, esto requiere un vector de control aleatorio en otro registro, por lo que solo vale la pena si puede usarlo varias veces.
vpshufb
es utilizable porque los datos necesarios para cada carril están allí desde la transmisión, y el bit alto del control aleatorio pondrá a cero el elemento correspondiente.
Esta estrategia de transmisión + reproducción aleatoria podría ser buena en Ryzen;
Agner Fog no enumera los conteos de
vpmovsx/zx ymm
para
vpmovsx/zx ymm
en él.
No haga algo como una carga de 128 o 256 bits y luego baraje eso para alimentar más instrucciones
vpmovzx
.
El rendimiento total aleatorio probablemente ya sea un cuello de botella porque
vpmovzx
es aleatorio.
Intel Haswell / Skylake (los uarches AVX2 más comunes) tienen barajadas de 1 por reloj pero cargas de 2 por reloj.
Usar instrucciones de barajado adicionales en lugar de plegar operandos de memoria separados en
vpmovzxbd
es terrible.
Solo si puede
reducir el
conteo total de UOP como sugerí con broadcast-load + vpmovzxbd + vpshufb es una victoria.
¿Mi respuesta sobre los
valores de píxeles de bytes de escala (y = ax + b) con SSE2 (como flotantes)?
puede ser relevante para convertir de nuevo a
uint8_t
.
La parte posterior de la devolución de paquetes a bytes es semi-difícil si se hace con AVX2
packssdw/packuswb
, porque funcionan en el carril, a diferencia de
vpmovzx
.
Con solo AVX1, no AVX2 , debe hacer:
VPMOVZXBD xmm0, [rsi]
VPMOVZXBD xmm1, [rsi+4]
VINSERTF128 ymm0, ymm0, xmm1, 1 ; put the 2nd load of data into the high128 of ymm0
VCVTDQ2PS ymm0, ymm0 ; convert to packed float. Yes, works without AVX2
Por supuesto, nunca necesita una matriz de flotante, solo
__m256
vectores.
GCC / MSVC omitió optimizaciones para
VPMOVZXBD ymm,[mem]
con intrínsecos
GCC y MSVC son malos para plegar un
_mm_loadl_epi64
en un operando de memoria para
vpmovzx*
.
(Pero al menos hay una carga intrínseca del ancho correcto, a diferencia de
pmovzxbq xmm, word [mem]
).
Obtenemos una carga
vmovq
y luego una
vpmovzx
separada con una entrada XMM.
(Con ICC y clang3.6 + obtenemos un código seguro + óptimo al usar
_mm_loadl_epi64
, como gcc9 +)
Pero gcc8.3 y versiones anteriores
pueden
plegar una
_mm_loadu_si128
16 bytes intrínseca en un operando de memoria de 8 bytes.
Esto proporciona un asm óptimo en
-O3
en GCC, pero no es seguro en
-O0
donde se compila en una carga
vmovdqu
real que toca más datos de los que realmente
vmovdqu
, y podría salirse del final de una página.
Dos errores de gcc enviados debido a esta respuesta:
- La carga movq SSE / AVX (_mm_cvtsi64_si128) no se pliega en pmovzx ( arreglado para gcc9 , pero la corrección rompe la carga plegándose para una carga de 128 bits, por lo que el hack de la solución alternativa para el GCC anterior empeora gcc9).
-
No intrínseco para x86
MOVQ m64, %xmm
en modo de 32 bits . (TODO: ¿informar esto también para clang / LLVM?)
No es intrínseco usar SSE4.1
pmovsx
/
pmovzx
como carga, solo con un operando fuente
__m128i
.
Pero las instrucciones asm solo leen la cantidad de datos que realmente usan, no un operando fuente de memoria
__m128i
16 bytes.
A diferencia de
punpck*
, puede usar esto en los últimos 8B de una página sin fallar.
(Y en direcciones no alineadas incluso con la versión no AVX).
Así que aquí está la solución malvada que se me ocurrió.
¡No use esto,
#ifdef __OPTIMIZE__
es malo, lo que hace posible crear errores que solo suceden en la compilación de depuración o solo en la compilación optimizada!
#if !defined(__OPTIMIZE__)
// Making your code compile differently with/without optimization is a TERRIBLE idea
// great way to create Heisenbugs that disappear when you try to debug them.
// Even if you *plan* to always use -Og for debugging, instead of -O0, this is still evil
#define USE_MOVQ
#endif
__m256 load_bytes_to_m256(uint8_t *p)
{
#ifdef USE_MOVQ // compiles to an actual movq then movzx ymm, xmm with gcc8.3 -O3
__m128i small_load = _mm_loadl_epi64( (const __m128i*)p);
#else // USE_LOADU // compiles to a 128b load with gcc -O0, potentially segfaulting
__m128i small_load = _mm_loadu_si128( (const __m128i*)p );
#endif
__m256i intvec = _mm256_cvtepu8_epi32( small_load );
//__m256i intvec = _mm256_cvtepu8_epi32( *(__m128i*)p ); // compiles to an aligned load with -O0
return _mm256_cvtepi32_ps(intvec);
}
Con USE_MOVQ habilitado,
emite
gcc -O3
-O3 (v5.3.0)
.
(También lo hace MSVC)
load_bytes_to_m256(unsigned char*):
vmovq xmm0, QWORD PTR [rdi]
vpmovzxbd ymm0, xmm0
vcvtdq2ps ymm0, ymm0
ret
El estúpido
vmovq
es lo que queremos evitar.
Si deja que use la versión insegura
loadu_si128
, será un buen código optimizado.
GCC9, clang e ICC emiten:
load_bytes_to_m256(unsigned char*):
vpmovzxbd ymm0, qword ptr [rdi] # ymm0 = mem[0],zero,zero,zero,mem[1],zero,zero,zero,mem[2],zero,zero,zero,mem[3],zero,zero,zero,mem[4],zero,zero,zero,mem[5],zero,zero,zero,mem[6],zero,zero,zero,mem[7],zero,zero,zero
vcvtdq2ps ymm0, ymm0
ret
Escribir la versión AVX1 solo con intrínsecos se deja como un ejercicio poco divertido para el lector.
Pediste "instrucciones", no "intrínsecas", y este es un lugar donde hay una brecha en las intrínsecas.
Tener que usar
_mm_cvtsi64_si128
para evitar la carga potencial de direcciones fuera de límites es estúpido, en mi opinión.
Quiero poder pensar en los intrínsecos en términos de las instrucciones a las que se asignan, con los intrínsecos de carga / almacenamiento como información al compilador sobre las garantías de alineación o la falta de ellas.
Tener que usar lo intrínseco para una instrucción que no quiero es bastante tonto.
También tenga en cuenta que si está buscando en el manual de Intel Insn Ref, hay dos entradas separadas para movq:
-
movd / movq, la versión que puede tener un registro entero como un operando src / dest (
66 REX.W 0F 6E
(oVEX.128.66.0F.W1 6E
) para (V) MOVQ xmm, r / m64). Ahí es donde encontrará el intrínseco que puede aceptar un número entero de 64 bits,_mm_cvtsi64_si128
. (Algunos compiladores no lo definen en modo de 32 bits). -
movq: la versión que puede tener dos registros xmm como operandos. Esta es una extensión de la instrucción MMXreg -> MMXreg, que también puede cargar / almacenar, como MOVDQU. Su código de operación
F3 0F 7E
(VEX.128.F3.0F.WIG 7E
) paraMOVQ xmm, xmm/m64)
.El manual de referencia asm ISA solo enumera el
m128i _mm_mov_epi64(__m128i a)
intrínseco para poner a cero los 64b altos de un vector mientras lo copia. Pero la guía intrínseca enumera_mm_loadl_epi64(__m128i const* mem_addr)
que tiene un prototipo estúpido (puntero a un tipo__m128i
16 bytes cuando en realidad solo carga 8 bytes). Está disponible en los 4 principales compiladores x86, y en realidad debería ser seguro. Tenga en cuenta que el__m128i*
acaba de pasar a este opaco intrínseco, en realidad no desreferenciado.El
_mm_loadu_si64 (void const* mem_addr)
más sano_mm_loadu_si64 (void const* mem_addr)
también está en la lista, pero a gcc le falta ese.
Estoy optimizando un algoritmo para el desenfoque gaussiano en una imagen y quiero reemplazar el uso de un búfer flotante [8] en el código a continuación con una variable intrínseca __m256. ¿Qué serie de instrucciones es la más adecuada para esta tarea?
// unsigned char *new_image is loaded with data
...
float buffer[8];
buffer[x ] = new_image[x];
buffer[x + 1] = new_image[x + 1];
buffer[x + 2] = new_image[x + 2];
buffer[x + 3] = new_image[x + 3];
buffer[x + 4] = new_image[x + 4];
buffer[x + 5] = new_image[x + 5];
buffer[x + 6] = new_image[x + 6];
buffer[x + 7] = new_image[x + 7];
// buffer is then used for further operations
...
//What I want instead in pseudocode:
__m256 b = [float(new_image[x+7]), float(new_image[x+6]), ... , float(new_image[x])];