c++ sse simd avx avx2

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:

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 (o VEX.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 ) para MOVQ 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])];