¿Cuáles son las mejores secuencias de instrucciones para generar constantes vectoriales sobre la marcha?

“Mejor” significa la menor cantidad de instrucciones (o la menor cantidad de uops, si alguna instrucción decodifica a más de un uop). El tamaño del código de máquina en bytes es un desempate para igual número de ins.

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 el rendimiento y las demandas de 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 precioso espacio de uop-caché. Esto puede ser un recurso aún más limitado que la caché de datos.

La excelente guía de Optimizing Assembly 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 elemento de 8 a 64 bits. La Tabla 13.11 tiene secuencias para generar algunos valores de punto 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 por un tiempo.

¿Qué otras constantes se pueden generar con secuencias cortas de instrucciones no obvias? (Las extensiones adicionales con diferentes recuentos de cambios son obvias y no “interesantes”.) ¿Hay mejores secuencias para generar las constantes que Agner Fog enumera?

Cómo mover los eventos inmediatos de 128 bits a los registros XMM ilustra algunas formas de poner una constante arbitraria 128b en la secuencia de instrucciones, pero eso no suele ser razonable (no ahorra espacio y ocupa mucho espacio de uop-cache).

Todo-cero: pxor xmm0,xmm0 (o xorps xmm0,xmm0 , un byte de instrucción más corto.)

Todo en uno: 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 CPUs antiguas como K10 y pre-Core2 P6). No hay ninguna ventaja para la versión W sobre las versiones de tamaño de elemento de byte o dword de pcmpeq en cualquier CPU en las tablas de instrucciones de Agner Fog, pero pcmpeqQ toma un byte extra, 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 tabla ascii-art, pero espero que las mejoras se incluirán en futuras versiones de la guía.


La principal dificultad son los vectores de 8 PSLLB , porque no hay PSLLB

La tabla de Agner Fog genera vectores de elementos de 16 bits y usa packuswb para evitar 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 turnos, 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 hacia 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 shift-by-one a la izquierda 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-uno ( -1 ) en un vector de 1 bytes , por lo que solo se necesitan dos instrucciones. Podemos generar 2 bytes con pcmpeqw / paddb / pabsb . (El orden de agregar vs. abs no importa). pabs no necesita un imm8, pero solo guarda los bytes de código para otros anchos de elemento vs. 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 lo contrario puede usar un prefijo VEX de 2 bytes para su VEX.NDD.128.66.0F.WIG )

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 turnos pueden poner el único bit establecido 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 solo de escritura, y no necesita ser el mismo que el de la fuente para obtener el comportamiento deseado). Puede mantener los todo alrededor como una constante, o como el inicio de la generación de otra constante, o como un operando fuente 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 de memoria que se saturan correctamente son objectives 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 solo generarlo como un valor en cada palabra y hacer el packuswb habitual.

pavgb (SSE2) contra un registro a cero puede desplazarse hacia la derecha en uno, pero solo si el valor es par. (Hace sin signo dst = (dst+src+1)>>1 para el 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 impar: pxor xmm1,xmm1 / pcmpeqw xmm0,xmm0 / paddb xmm0,xmm0 / pavgb xmm0, xmm1 produce 0x7f bytes con una entrada más que desplazamiento / paquete. Sin embargo, si un registro a cero ya es necesario para otra cosa, paddb / pavgb guarda un byte de instrucción.


He probado estas secuencias La forma más fácil es lanzarlos en .asm , ensamblar / vincular y ejecutar gdb en él. layout asm , display /x $xmm0.v16_int8 para volcar eso después de cada paso único, y las instrucciones de un solo paso ( ni o si ). En el modo layout reg , puede hacer tui reg vec para cambiar a una visualización de regs vectoriales, pero es casi inútil porque no puede seleccionar qué interpretación mostrar (siempre los obtiene, no puede desplazarse, y el las columnas no se alinean entre los registros). Sin embargo, es excelente para regs / flags enteros.


Tenga en cuenta que el uso de estos con intrínsecos puede ser complicado. A los comstackdores no les gusta operar en variables no inicializadas, por lo que debe usar _mm_undefined_si128() para decirle al comstackdor que es lo que usted quiso decir. O tal vez al usar _mm_set1_epi32(-1) obtendrá su comstackdor para emitir un pcmpeqd same,same . Sin esto, algunos comstackdores serán xor-zero variables vectoriales sin inicializar antes del uso, o incluso (MSVC) cargan la memoria no inicializada de la stack.


Muchas constantes se pueden almacenar de forma más compacta en la memoria aprovechando el pmovzx o pmovsx de pmovsx para cero o extensión de signo sobre la marcha. Por ejemplo, un vector 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 pueden micro fusibles con pmovzx , por lo que no necesitan ningún uops de dominio fusionado adicional. Sin embargo, evita el uso de 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 ninguna versión que tome un operando de puntero uint32_t * . Puede hackearlo, pero es feo y la falla de la optimización del comstackdor es un problema. Consulte la pregunta vinculada para obtener detalles y enlaces a los informes de errores de gcc.

Con las constantes 256b y (no tan pronto) de 512b, los ahorros en la memoria son mayores. Sin embargo, esto solo es importante si múltiples constantes útiles pueden compartir una línea de caché.

El equivalente FP de esto es VCVTPH2PS xmm1, xmm2/m64 , que requiere el indicador de función F16C (semicre precisión). (También hay una instrucción de tienda que se empaqueta de a la mitad, pero no de computación con la mitad de precisión. Solo se trata de una optimización de huella de ancho de banda / memoria caché).


Obviamente, cuando todos los elementos son iguales (pero no son aptos para generar sobre la marcha), pshufd o AVX vbroadcastps / AVX2 vpbroadcastb/w/d/q/i128 son útiles. pshufd puede tomar un operando fuente de memoria, pero tiene que ser 128b. movddup (SSE3) hace una carga de 64 bits, se transmite para llenar un registro 128b. En Intel, no necesita una unidad de ejecución ALU, solo carga el puerto. (De manera similar, AVX v[p]broadcast cargas de tamaño dword y mayores se manejan en la unidad de carga, sin ALU).

Broadcasts o pmovz/sx son excelentes para guardar el tamaño del ejecutable cuando va a cargar una máscara en un registro para uso repetido en un bucle. Generar varias máscaras similares desde un punto de partida también puede ahorrar espacio, si solo lleva una instrucción.

Consulte también Para para un vector SSE que tiene todos los mismos componentes, ¿genera sobre la marcha o precomputa? que está pidiendo más sobre el uso de set1 intrínseco, y no está claro si está preguntando sobre constantes o emisiones de variables.

También experimenté algunos con la salida del comstackdor para las transmisiones .


Si las fallas de caché son un problema , eche un vistazo a su código y vea si el comstackdor ha duplicado _mm_set constantes _mm_set cuando la misma función está en línea en llamadas diferentes. También tenga cuidado con las constantes que se utilizan juntas (por ejemplo, en funciones llamadas una tras otra) dispersas en diferentes líneas de caché. Muchas cargas dispersas para constantes son mucho peores que cargar muchas constantes, todas cercanas entre sí.

pmovzx y / o broadcast le permiten empacar más constantes en una línea de caché, con muy poca sobrecarga para cargarlas en un registro. La carga no estará en la ruta crítica, por lo que incluso si toma un uop extra, puede tomar una unidad de ejecución libre en cualquier ciclo durante una ventana larga.

clang en realidad hace un buen trabajo al respecto : las constantes de set1 separadas en diferentes funciones se reconocen como idénticas, de la misma forma en que se pueden fusionar literales de cadena idénticos. Tenga en cuenta que la salida de origen del ASM de clang parece mostrar que cada función tiene su propia copia de la constante, pero el desassembly binario muestra que todas esas direcciones efectivas relativas al 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 , así que claramente los desarrolladores de clang se dieron cuenta de que el tamaño importa para el rendimiento, no solo para -Os ). IDK por qué no se reduce a una constante 4B con vbroadcastss , porque eso debería ser igual de rápido. Desafortunadamente, el vbroadcast no proviene simplemente de parte de la constante 16B sino de otras funciones utilizadas. Esto tal vez 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 hacer 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 de ensamblar o vincular (como sea que esté hecho. No leí cada directiva para descubrir cuál permite la fusión).

gcc 5.3 también combina constantes, pero no usa cargas de difusión para comprimir las constantes de 32B. De nuevo, la constante 16B no se superpone con la constante 32B.