x86 - ¿hay una instrucción inversa a la instrucción movemask en intel avx2?
intrinsics icc (1)
No hay una sola instrucción en AVX2 o anterior.
- 4 bits -> 4 qwords en un registro YMM: esta respuesta: un LUT es bueno, ALU también es bueno
- 8 bits -> 8 dwords en un registro YMM: esta respuesta: ALU es bueno
-
16 bits -> 16 palabras
: esta respuesta con
vpbroadcastw
/vpand
/vpcmpeqw
-
32 bits -> 32 bytes
:
¿Cómo realizar el inverso de _mm256_movemask_epi8 (VPMOVMSKB)?
También la forma más rápida de descomprimir 32 bits en un vector SIMD de 32 bytes .
Si está cargando el mapa de bits desde la memoria, cargarlo directamente en los registros vectoriales para una estrategia ALU debería funcionar bien.
Si tiene el mapa de bits como resultado del cálculo, estará en un registro entero donde podrá usarlo fácilmente como un índice LUT, por lo que es una buena opción si apunta a elementos de 64 bits. De lo contrario, probablemente siga utilizando la ALU para elementos de 32 bits o más pequeños, en lugar de una LUT gigante o haciendo múltiples fragmentos.
Tendremos que esperar los registros de máscara de AVX-512 antes de que sea posible la conversión barata de máscaras de bits enteras a máscaras vectoriales.
(Con
kmovw k1, r/m16
, que los compiladores generan implícitamente para
int => __mmask16
).
Hay un AVX512 insn para establecer un vector desde una máscara (
VPMOVM2D zmm1, k1
,
_mm512_movm_epi8/16/32/64
, con otras versiones para diferentes tamaños de elementos), pero generalmente no lo necesita ya que todo lo que solía usar vectores de máscara ahora usa registros de máscara.
¿Tal vez si desea contar elementos que cumplen alguna condición de comparación?
(donde usaría
pcmpeqd
/
psubd
para generar y acumular el vector de 0 o -1 elementos).
Pero la
popcnt
escalar de los resultados de la máscara sería una mejor apuesta.
Pero tenga en cuenta que
vpmovm2d
requiere que la máscara esté en un
k0..7
máscara AVX512
k0..7
.
Para llegar allí, se necesitarán instrucciones adicionales a menos que provengan de un resultado de comparación de vectores, y las instrucciones que se mueven a los registros de máscara necesitan un UOP para el puerto 5 en Intel Skylake-X y CPU similares, por lo que esto puede ser un cuello de botella (especialmente si haces cualquier barajadura) )
Especialmente si comienza en la memoria (cargando un mapa de bits) y solo necesita el bit alto de cada elemento, probablemente aún esté mejor con una carga de transmisión + cambio variable, incluso si las instrucciones AVX512 de 256 y 512 bits están disponibles.
Para elementos de 64 bits, la máscara solo tiene 4 bits, por lo que una tabla de búsqueda es razonable
.
Puede comprimir el LUT cargándolo con
VPMOVSXBQ ymm1, xmm2/m32
.
(
_mm256_cvtepi8_epi64
)
.
Esto le da un tamaño LUT de (1 << 4) = 16 * 4 bytes = 64B = 1 línea de caché.
Desafortunadamente,
pmovsx
es inconveniente para usar como una carga estrecha con intrínsecos
.
Especialmente si ya tiene su mapa de bits en un registro entero (en lugar de memoria), un LUT
vpmovsxbq
debería ser excelente dentro de un bucle interno para elementos de 64 bits.
O si el rendimiento de la instrucción o el rendimiento aleatorio es un cuello de botella, use un LUT sin comprimir.
Esto puede permitirle (o al compilador) usar el vector de máscara como un operando de memoria para otra cosa, en lugar de necesitar una instrucción separada para cargarlo.
LUT para elementos de 32 bits: probablemente no sea óptimo, pero así es como podría hacerlo
Con elementos de 32 bits, una máscara de 8 bits le proporciona 256 vectores posibles, cada uno de 8 elementos de longitud.
256 * 8B = 2048 bytes, que es una huella de caché bastante grande incluso para la versión comprimida (carga con
vpmovsxbd ymm, m64
).
Para evitar esto, puede dividir el LUT en fragmentos de 4 bits
.
Se necesitan aproximadamente 3 instrucciones enteras para dividir un entero de 8 bits en dos enteros de 4 bits (
mov/and/shr
).
Luego, con una LUT sin comprimir de vectores de 128b (para un tamaño de elemento de 32 bits),
vmovdqa
la mitad baja y
vinserti128
la mitad alta.
Todavía podría comprimir el LUT, pero no lo recomendaría porque necesitará
vmovd
/
vpinsrd
/
vpmovsxbd
, que es 2 shuffles (por lo que probablemente tenga un cuello de botella en el rendimiento de uop).
O 2x
vpmovsxbd xmm, [lut + rsi*4]
+
vinserti128
es probablemente aún peor en Intel.
Alternativa ALU: buena para elementos de 16/32/64 bits
Cuando todo el mapa de bits encaja en cada elemento, transmítalo, Y con una máscara de selección, y VPCMPEQ contra la misma constante (que puede permanecer en un registro a través de múltiples usos de este en un bucle).
vpbroadcastd ymm0, dword [mask]
vpand ymm0, ymm0, [vec of 1<<0, 1<<1, 1<<2, 1<<3, ...]
vpcmpeqd ymm0, ymm0, [same constant]
; ymm0 = (mask & bit) == bit
; where bit = 1<<element_number
(La máscara podría provenir de un registro entero con vmovd + vpbroadcastd, pero una carga de difusión
Para elementos de 8 bits, necesitará
vpshufb
el resultado de
vpbroadcastd
para obtener el bit relevante en cada byte.
Consulte
Cómo realizar el inverso de _mm256_movemask_epi8 (VPMOVMSKB)?
.
Pero para elementos de 16 bits y más anchos, el número de elementos es <= el ancho del elemento, por lo que una carga de difusión lo hace de forma gratuita.
(Las cargas de difusión de 16 bits cuestan un UOP aleatorio micro fusionado, a diferencia de las cargas de difusión de 32 y 64 bits que se manejan completamente en los puertos de carga).
vpbroadcastd/q
ni siquiera cuesta ninguna unidad de ALU, se hace directamente en el puerto de carga.
(
w
son load + shuffle).
Incluso si sus máscaras están empaquetadas (una por byte para elementos de 32 o 64 bits), podría ser más eficiente
vpbroadcastd
lugar de
vpbroadcastb
.
La comprobación de
x & mask == mask
no se preocupa por la basura en los bytes altos de cada elemento después de la transmisión.
La única preocupación son las divisiones de línea / página de caché.
Cambio variable (más barato en Skylake) si necesita solo el bit de signo
Las mezclas variables y las cargas / tiendas enmascaradas solo se preocupan por el bit de signo de los elementos de máscara.
Esto es solo 1 uop (en Skylake) una vez que se transmite la máscara de 8 bits a los elementos dword.
vpbroadcastd ymm0, dword [mask]
vpsllvd ymm0, ymm0, [vec of 24, 25, 26, 27, 28, 29, 30, 31] ; high bit of each element = corresponding bit of the mask
;vpsrad ymm0, ymm0, 31 ; broadcast the sign bit of each element to the whole element
;vpsllvd + vpsrad has no advantage over vpand / vpcmpeqb, so don''t use this if you need all the bits set.
vpbroadcastd
es tan barato como una carga de memoria (no hay ALU uop en absoluto en las CPU Intel y Ryzen).
(Las transmisiones más estrechas, como
vpbroadcastb y,mem
toman un ALU shuffle uop en Intel, pero tal vez no en Ryzen).
El cambio variable es un poco costoso en Haswell / Broadwell (3 uops, puertos de ejecución limitados), ¡pero tan barato como los cambios de conteo inmediato en Skylake! (1 uop en el puerto 0 o 1.) En Ryzen también son solo 2 uops (el mínimo para cualquier operación de 256b), pero tienen una latencia de 3c y una por rendimiento de 4c.
Consulte la wiki de etiquetas x86 para obtener información sobre el rendimiento , especialmente las tablas de información de Agner Fog .
Para los elementos de 64 bits, tenga en cuenta que los cambios aritméticos a la derecha solo están disponibles en tamaños de elementos de 16 y 32 bits. Use una estrategia diferente si desea que todo el elemento se establezca en todo-cero / todo-uno para 4 bits -> elementos de 64 bits.
Con intrínsecos:
__m256i bitmap2vecmask(int m) {
const __m256i vshift_count = _mm256_set_epi32(24, 25, 26, 27, 28, 29, 30, 31);
__m256i bcast = _mm256_set1_epi32(m);
__m256i shifted = _mm256_sllv_epi32(bcast, vshift_count); // high bit of each element = corresponding bit of the mask
return shifted;
// use _mm256_and and _mm256_cmpeq if you need all bits set.
//return _mm256_srai_epi32(shifted, 31); // broadcast the sign bit to the whole element
}
Dentro de un bucle, una LUT podría valer la huella del caché, dependiendo de la mezcla de instrucciones en el bucle. Especialmente para el tamaño de elemento de 64 bits donde no hay mucha huella de caché, pero posiblemente incluso para 32 bits.
Otra opción, en lugar de desplazamiento variable, es usar BMI2 para desempaquetar cada bit en un byte con ese elemento de máscara en el bit alto, luego
vpmovsx
:
; 8bit mask bitmap in eax, constant in rdi
pdep rax, rax, rdi ; rdi = 0b1000000010000000... repeating
vmovq xmm0, rax
vpmovsxbd ymm0, xmm0 ; each element = 0xffffff80 or 0
; optional
;vpsrad ymm0, ymm0, 8 ; arithmetic shift to get -1 or 0
Si ya tiene máscaras en un registro de enteros (donde tendría que
vmovq
/
vpbroadcastd
separado de todos modos), entonces esta manera probablemente sea mejor incluso en Skylake, donde los cambios de conteo variable son baratos.
Si sus máscaras comienzan en la memoria, el otro método ALU (
vpbroadcastd
directamente en un vector) es probablemente mejor, porque las cargas de difusión son muy baratas.
Tenga en cuenta que
pdep
es 6 uops dependientes de Ryzen (latencia de 18c, rendimiento de 18c), por lo que este método es horrible en Ryzen incluso si sus máscaras comienzan en registros enteros.
(Futuros lectores, siéntanse libres de editar en una versión intrínseca de esto. Es más fácil escribir asm porque es mucho menos tipeado, y los mnemónicos asm son más fáciles de leer (no hay estúpido
_mm256_
desorden en todo el lugar)).
Las instrucciones de la máscara de movimiento toman un __m256i y devuelven un int32 donde cada bit (ya sea los primeros 4, 8 o los 32 bits, dependiendo del tipo de elemento del vector de entrada) es el bit más significativo del elemento del vector correspondiente.
Me gustaría hacer lo inverso: tomar un 32 (donde solo los 4, 8 o 32 bits menos significativos son significativos), y obtener un __m256i donde el bit más significativo de cada bloque de tamaño int8, int32 o int64 se establece en el original poco.
Básicamente, quiero pasar de una máscara de bits comprimida a una que se pueda usar como máscara mediante otras instrucciones AVX2 (como maskstore, maskload, mask_gather).
No pude encontrar rápidamente una instrucción que lo haga, así que pregunto aquí. Si no hay una sola instrucción con esa funcionalidad, ¿hay algún truco inteligente que se te ocurra que logre esto en muy pocas instrucciones?
Mi método actual es usar una tabla de búsqueda de 256 elementos. Quiero usar esta operación dentro de un bucle donde no sucede mucho más, para acelerarlo. Tenga en cuenta que no estoy demasiado interesado en secuencias largas de múltiples instrucciones o pequeños bucles que implementan esta operación.