assembly - significado - ¿Cuáles son las mejores secuencias de instrucciones para generar constantes vectoriales sobre la marcha?
sse2 procesador (1)
Todo cero:
pxor xmm0,xmm0
(o
xorps xmm0,xmm0
, un byte de instrucción más corto). No hay mucha diferencia en las CPU modernas, pero en Nehalem (antes de la eliminación de xor-zero), el xorps uop solo podía ejecutarse en puerto 5. Creo que es por eso que los compiladores
pxor
pxor
pxor
cero incluso para los registros que se utilizarán con instrucciones de FP.
Todos:
pcmpeqw xmm0,xmm0
.
Este es el punto de partida habitual para generar otras constantes, porque (como
pxor
) rompe la dependencia del valor anterior del registro (excepto en CPU antiguas como K10 y pre-Core2 P6).
La versión
W
no tiene ninguna ventaja sobre las versiones de tamaño de elemento byte o dword de
pcmpeq
en cualquier CPU en las tablas de instrucciones de Agner Fog, pero
pcmpeqQ
toma un byte adicional, es más lento en Silvermont y requiere SSE4.1.
SO realmente no tiene formato de tabla , así que solo voy a enumerar las adiciones a la tabla 13.10 de Agner Fog, en lugar de una versión mejorada. Lo siento. Tal vez si esta respuesta se vuelve popular, usaré un generador de tablas ascii-art, pero espero que las mejoras se incluyan en futuras versiones de la guía.
La principal dificultad son los vectores de 8 bits, porque
no hay
PSLLB
La tabla de Agner Fog genera vectores de elementos de 16 bits y usa
packuswb
para
packuswb
esto.
Por ejemplo,
pcmpeqw xmm0,xmm0
/
psrlw xmm0,15
/
psllw xmm0,1
/
packuswb xmm0,xmm0
genera un vector donde cada byte es
2
.
(Este patrón de cambios, con diferentes recuentos, es la forma principal de producir la mayoría de las constantes para vectores más amplios).
Hay una mejor manera:
paddb xmm0,xmm0
(SSE2) funciona como un desplazamiento a la izquierda por uno con granularidad de bytes, por lo que se puede generar un
vector de
-2
bytes
con solo dos instrucciones (
pcmpeqw
/
paddb
).
paddw/d/q
como un desplazamiento a la izquierda por uno para otros tamaños de elementos ahorra un byte de código de máquina en comparación con los cambios, y generalmente puede ejecutarse en más puertos que un shift-imm.
pabsb xmm0,xmm0
(SSSE3) convierte un vector de todos-unos (
-1
) en un
vector de
1
bytes
, y no es destructivo, por lo que todavía tiene el
set1(-1)
.
(A veces no necesitas
set1(1)
. Puedes sumar 1 a cada elemento restando
-1
con
psubb
).
Podemos generar
2
bytes
con
pcmpeqw
/
paddb
/
pabsb
.
(El orden de agregar vs abs no importa).
pabs
no necesita un imm8, pero solo guarda bytes de código para otros anchos de elementos frente al desplazamiento a la derecha cuando ambos requieren un prefijo VEX de 3 bytes.
Esto solo ocurre cuando el registro fuente es xmm8-15.
(
vpabsb/w/d
siempre requiere un prefijo VEX de 3 bytes para
VEX.128.66.0F38.WIG
, pero
vpsrlw dest,src,imm
puede usar un prefijo VEX de 2 bytes para su
VEX.NDD.128.66.0F.WIG
)
De hecho, también podemos guardar instrucciones para generar
4
bytes
:
pcmpeqw
/
pabsb
/
psllw xmm0, 2
.
Todos los bits que se desplazan a través de los límites de bytes por el cambio de palabra son cero, gracias a
pabsb
.
Obviamente, otros recuentos de desplazamiento pueden colocar el bit de conjunto único en otras ubicaciones, incluido el bit de signo para generar un vector de
-128 (0x80) bytes
.
Tenga en cuenta que
pabsb
no es destructivo (el operando de destino es de solo escritura y no necesita ser el mismo que el de origen para obtener el comportamiento deseado).
Puede mantener todos alrededor como una constante, o como el comienzo de generar otra constante, o como un operando de origen para
psubb
(para incrementar en uno).
También se puede generar un
vector de
0x80
bytes
(ver párrafo anterior) a partir de cualquier cosa que se sature a -128, usando
packsswb
.
por ejemplo, si ya tiene un vector de
0xFF00
para otra cosa, simplemente cópielo y use
packsswb
.
Las constantes cargadas desde la memoria que se saturan correctamente son objetivos potenciales para esto.
Se puede generar un
vector de
0x7f
bytes
con
pcmpeqw
/
psrlw xmm0, 9
/
packuswb xmm0,xmm0
.
Estoy contando esto como "no obvio" porque la naturaleza mayoritariamente establecida no me hizo pensar en generarlo solo como un valor en cada palabra y hacer el
packuswb
habitual.
pavgb
(SSE2) contra un registro puesto a cero puede desplazarse a la derecha en uno, pero solo si el valor es par.
(Hace
dst = (dst+src+1)>>1
sin signo
dst = (dst+src+1)>>1
para redondeo, con precisión interna de 9 bits para el temporal.) Sin embargo, esto no parece ser útil para la generación constante, porque 0xff es extraño:
pxor xmm1,xmm1
/
pcmpeqw xmm0,xmm0
/
paddb xmm0,xmm0
/
pavgb xmm0, xmm1
produce
0x7f
bytes
con una entrada más que shift / pack.
Sin embargo, si ya se necesita un registro a cero para otra cosa,
paddb
/
pavgb
guarda un byte de instrucción.
He probado estas secuencias.
La forma más fácil es lanzarlos en un
.asm
, ensamblar / vincular y ejecutar gdb en él.
layout asm
,
display /x $xmm0.v16_int8
para volcar eso después de cada paso individual y las instrucciones de paso único (
ni
o
si
).
En el modo de
layout reg
, puede hacer
tui reg vec
para cambiar a una visualización de
tui reg vec
de vectores, pero es casi inútil porque no puede seleccionar qué interpretación mostrar (siempre obtiene todos ellos, y no puede hscroll, y el las columnas no se alinean entre los registros).
Sin embargo, es excelente para enteros regs / flags.
Tenga en cuenta que usar estos con intrínsecos puede ser complicado.
A los compiladores no les gusta operar con variables no inicializadas, por lo que debes usar
_mm_undefined_si128()
para decirle al compilador que eso es lo que
_mm_undefined_si128()
decir.
O tal vez usando
_mm_set1_epi32(-1)
conseguirá que su compilador emita un
pcmpeqd same,same
.
Sin esto, algunos compiladores xor-cero variables vectoriales no inicializadas antes de su uso, o incluso (MSVC) cargar la memoria no inicializada de la pila.
Muchas constantes se pueden almacenar de forma más compacta en la memoria aprovechando
pmovzx
o
pmovsx
de
pmovsx
para cero o extensión de signo sobre la marcha.
Por ejemplo, un vector de 128b de
{1, 2, 3, 4}
como elementos de 32 bits podría generarse con una carga
pmovzx
desde una ubicación de memoria de 32 bits.
Los operandos de memoria se pueden fusionar con
pmovzx
, por lo que no se necesitan uops adicionales de dominio fusionado.
Sin embargo, evita usar la constante directamente como un operando de memoria.
El
soporte intrínseco de
C / C ++
para usar
pmovz/sx
como carga es terrible
: hay
_mm_cvtepu8_epi32 (__m128i a)
, pero no hay una versión que tome un operando de puntero
uint32_t *
.
Puedes hackearlo, pero es feo y la falla de optimización del compilador es un problema.
Consulte la pregunta vinculada para obtener detalles y enlaces a los informes de errores de gcc.
Con 256b y (no tan) pronto constantes de 512b, los ahorros en memoria son mayores. Sin embargo, esto solo es muy importante si varias constantes útiles pueden compartir una línea de caché.
El equivalente de FP de esto es
VCVTPH2PS xmm1, xmm2/m64
, que requiere el indicador de función F16C (media precisión).
(También hay una instrucción de tienda que empaqueta de uno a la mitad, pero no hay cálculo con la mitad de precisión. Es solo una optimización de ancho de banda de memoria / huella de caché).
Obviamente, cuando todos los elementos son iguales (pero no adecuados para generar sobre la marcha),
pshufd
o AVX
vbroadcastps
/ AVX2
vpbroadcastb/w/d/q/i128
son útiles.
pshufd
puede tomar un operando de origen de memoria, pero tiene que ser 128b.
movddup
(SSE3) realiza una carga de 64 bits, se difunde para llenar un registro de 128b.
En Intel, no necesita una unidad de ejecución ALU, solo carga el puerto.
(Del mismo modo, AVX
v[p]broadcast
cargas de tamaño dword y mayores se manejan en la unidad de carga, sin ALU).
Las transmisiones o
pmovz/sx
son excelentes para guardar el tamaño ejecutable
cuando va a cargar una máscara en un registro para su uso repetido en un bucle.
La generación de máscaras similares a partir de un punto de partida también puede ahorrar espacio, si solo se necesita una instrucción.
Consulte también ¿
Para un vector SSE que tiene todos los mismos componentes, generar sobre la marcha o calcular previamente?
que pregunta más sobre el uso del
set1
intrínseco, y no está claro si se trata de constantes o transmisiones de variables.
También experimenté algunos con la salida del compilador para transmisiones .
Si las fallas de caché son un problema
, eche un vistazo a su código y vea si el compilador ha duplicado
_mm_set
constantes cuando la misma función está insertada en diferentes llamantes.
También tenga cuidado con las constantes que se usan juntas (por ejemplo, en funciones llamadas una tras otra) que se dispersan en diferentes líneas de caché.
Muchas cargas dispersas para constantes son mucho peores que cargar muchas constantes, todas cerca una de la otra.
pmovzx
y / o broadcast le permiten empaquetar más constantes en una línea de caché, con una sobrecarga muy baja para cargarlas en un registro.
La carga no estará en la ruta crítica, por lo que incluso si toma un impulso adicional, puede tomar una unidad de ejecución libre en cualquier ciclo en una ventana larga.
clang realmente hace un buen trabajo de esto
: las constantes
set1
separadas en diferentes funciones se reconocen como idénticas, de la misma manera que se pueden fusionar literales de cadena idénticos.
Tenga en cuenta que la salida de fuente asm de clang parece mostrar que cada función tiene su propia copia de la constante, pero el desmontaje binario muestra que todas esas direcciones efectivas relativas a RIP hacen referencia a la misma ubicación.
Para las versiones de 256b de las funciones repetidas, clang también usa
vbroadcastsd
para requerir solo una carga de 8B, a expensas de una instrucción adicional en cada función.
(Esto está en
-O3
, por lo que claramente los desarrolladores clang se han dado cuenta de que el tamaño es importante para el rendimiento, no solo para
-Os
).
IDK por qué no se reduce a una constante de 4B con
vbroadcastss
, porque eso debería ser igual de rápido.
Desafortunadamente, el vbroadcast no proviene simplemente de parte de la constante 16B que usan las otras funciones.
Esto quizás tenga sentido: una versión AVX de algo probablemente solo podría fusionar algunas de sus constantes con una versión SSE.
Es mejor dejar las páginas de memoria con constantes SSE completamente frías, y que la versión AVX mantenga todas sus constantes juntas.
Además, es un problema de coincidencia de patrones más difícil de manejar en el momento del ensamblaje o el enlace (sin embargo, ya está hecho. No leí todas las directivas para averiguar cuál permite la fusión).
gcc 5.3 también combina constantes, pero no usa cargas de difusión para comprimir constantes de 32B. Nuevamente, la constante 16B no se superpone con la constante 32B.
"Mejor" significa la menor cantidad de instrucciones (o la menor cantidad de uops, si alguna de las instrucciones decodifica a más de una uop). El tamaño del código de máquina en bytes es un factor decisivo para un recuento de insn igual.
La generación constante es, por su propia naturaleza, el comienzo de una nueva cadena de dependencia, por lo que es inusual que la latencia sea importante. También es inusual generar constantes dentro de un bucle, por lo que las demandas de rendimiento y puerto de ejecución también son irrelevantes.
Generar constantes en lugar de cargarlas requiere más instrucciones (excepto para todo cero o todo uno), por lo que consume un valioso espacio de caché uop. Este puede ser un recurso aún más limitado que el caché de datos.
La excelente
guía de optimización de ensamblaje de
Agner Fog cubre esto en la
Section 13.4
.
La Tabla 13.10 tiene secuencias para generar vectores donde cada elemento es
0
,
1
,
2
,
3
,
4
,
-1
o
-2
, con tamaños de elementos de 8 a 64 bits.
La Tabla 13.11 tiene secuencias para generar algunos valores de coma flotante (
0.0
,
0.5
,
1.0
,
1.5
,
2.0
,
-2.0
y máscaras de bits para el bit de signo).
Las secuencias de Agner Fog solo usan SSE2, ya sea por diseño o porque no se ha actualizado durante un tiempo.
¿Qué otras constantes se pueden generar con secuencias cortas de instrucciones no obvias? (Otras extensiones con diferentes recuentos de cambios son obvias y no "interesantes".) ¿Hay mejores secuencias para generar las constantes que Agner Fog enumera?
Cómo mover 128 bits inmediatamente a los registros XMM ilustra algunas formas de poner una constante arbitraria de 128b en la secuencia de instrucciones, pero eso generalmente no es sensato (no ahorra espacio y ocupa mucho espacio uop-cache).