vectoriales tecnologia soporte que procesadores instrucciones extension conjunto con assembly x86 avx micro-optimization amd-processor

assembly - tecnologia - ¿Vxorps-zeroing en AMD Jaguar/Bulldozer/Zen es más rápido con registros xmm que ymm?



sse streaming simd extension (1)

Las CPU AMD manejan las instrucciones 256b AVX decodificando en dos operaciones 128b. ej. vaddps ymm0, ymm1,ymm1 en AMD Steamroller decodifica a 2 macrooperaciones, con la mitad del rendimiento de vaddps xmm0, xmm1,xmm1 .

XOR-zeroing es un caso especial (sin dependencia de entrada, y en Jaguar al menos evita consumir una entrada de archivo de registro físico , y permite que movdqa de ese registro se elimine en cuestión / cambio de nombre, como Bulldozer lo hace todo el tiempo incluso para no-zerod regs). ¿Pero se detecta lo suficientemente temprano que vxorps ymm0,ymm0,ymm0 todavía solo decodifica a 1 macrooperación con el mismo rendimiento que vxorps xmm0,xmm0,xmm0 ? (a diferencia de vxorps ymm3, ymm2,ymm1 )

¿O la detección de independencia ocurre más tarde, después de la descodificación en dos uops? Además, ¿el vector xor-zeroing en las CPU AMD todavía usa un puerto de ejecución? En Intel-CPU, Nehalem necesita un puerto, pero Sandybridge-family lo maneja en la etapa de problema / cambio de nombre.

Las tablas de instrucciones de Agner Fog no incluyen este caso especial, y su guía de microarch no menciona el número de uops.

Esto podría significar que vxorps xmm0,xmm0,xmm0 es una mejor manera de implementar _mm256_setzero_ps() .

Para AVX512, _mm512_setzero_ps() también guarda un byte usando solo una expresión de puesta a cero con código VEX, en lugar de EVEX, cuando sea posible. (es decir, para zmm0-15. vxorps xmm31,xmm31,xmm31 aún requeriría un EVEX). gcc / clang actualmente usa modismos xor-zeroing del ancho de registro que quiera, en lugar de usar siempre AVX-128.

Reportado como clang error 32862 y gcc error 80636 . MSVC ya usa xmm . Aún no se ha informado a ICC, que también utiliza reg zmm para la puesta a cero AVX512. (Aunque a Intel podría no importarle cambiar ya que actualmente no hay beneficio en ninguna CPU Intel, solo en AMD. Si alguna vez lanzan una CPU de bajo consumo que divide los vectores a la mitad, podrían hacerlo. Su deficiencia actual de baja potencia (Silvermont) no t admite AVX en absoluto, solo SSE4).

La única desventaja posible que conozco al usar una instrucción AVX-128 para poner a cero un registro de 256b es que no activa el calentamiento de las unidades de ejecución de 256b en las CPU de Intel. Posiblemente derrotando un truco de C o C ++ que intenta calentarlos.

(Las instrucciones vectoriales 256b son más lentas para los primeros ciclos ~ 56k después de la primera instrucción 256b. Consulte la sección Skylake en el pdf de microarrogación de Agner Fog). Probablemente esté bien si llamar a una función noinline que devuelve _mm256_setzero_ps no es una manera confiable de calentar las unidades de ejecución. (Uno que todavía funciona sin AVX2, y evita cualquier carga (que podría fallar el caché) es __m128 onebits = _mm_castsi128_ps(_mm_set1_epi8(0xff));
return _mm256_insertf128_ps(_mm256_castps128_ps256(onebits), onebits) que debería compilar a pcmpeqd xmm0,xmm0,xmm0 / vinsertf128 ymm0,xmm0,1 . Eso todavía es bastante trivial para algo que llamas una vez para calentar (o mantener caliente) las unidades de ejecución mucho antes que un ciclo crítico. Y si quieres algo que pueda alinearse, probablemente necesites asimetría en línea).

No tengo hardware AMD, así que no puedo probar esto.

Si alguien tiene hardware AMD pero no sabe cómo probarlo, use los contadores de rendimiento para contar los ciclos (y preferiblemente m-ops o uops o lo que sea que AMD los llame).

Esta es la fuente NASM / YASM que uso para probar secuencias cortas:

section .text global _start _start: mov ecx, 250000000 align 32 ; shouldn''t matter, but just in case .loop: dec ecx ; prevent macro-fusion by separating this from jnz, to avoid differences on CPUs that can''t macro-fuse %rep 6 ; vxorps xmm1, xmm1, xmm1 vxorps ymm1, ymm1, ymm1 %endrep jnz .loop xor edi,edi mov eax,231 ; exit_group(0) on x86-64 Linux syscall

Si no está en Linux, tal vez reemplace las cosas después del bucle (el syscall de salida) con un ret , y llame a la función desde una función C main() .

Ensamblar con nasm -felf64 vxor-zero.asm && ld -o vxor-zero vxor-zero.o para hacer un binario estático. (O utilice el script asm-link que publiqué en una sesión de preguntas y respuestas sobre el ensamblaje de binarios estáticos / dinámicos con / sin libc ).

Ejemplo de salida en un i7-6700k (Intel Skylake), a 3.9GHz. (IDK por qué mi máquina solo sube a 3.9GHz después de haber estado inactiva por unos minutos. Turbo hasta 4.2 o 4.4GHz funciona normalmente justo después del arranque). Como estoy usando contadores de percusión, en realidad no importa qué velocidad de reloj esté funcionando la máquina. No se trata de cargas / almacenamientos o errores de caché de código, por lo que la cantidad de ciclos de reloj central para todo es constante independientemente de cuánto tiempo permanezcan.

$ alias disas=''objdump -drwC -Mintel'' $ b=vxor-zero; asm-link "$b.asm" && disas "$b" && ocperf.py stat -etask-clock,cycles,instructions,branches,uops_issued.any,uops_retired.retire_slots,uops_executed.thread -r4 "./$b" + yasm -felf64 -Worphan-labels -gdwarf2 vxor-zero.asm + ld -o vxor-zero vxor-zero.o vxor-zero: file format elf64-x86-64 Disassembly of section .text: 0000000000400080 <_start>: 400080: b9 80 b2 e6 0e mov ecx,0xee6b280 400085: 66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 data16 data16 data16 data16 data16 nop WORD PTR cs:[rax+rax*1+0x0] 400094: 66 66 66 2e 0f 1f 84 00 00 00 00 00 data16 data16 nop WORD PTR cs:[rax+rax*1+0x0] 00000000004000a0 <_start.loop>: 4000a0: ff c9 dec ecx 4000a2: c5 f4 57 c9 vxorps ymm1,ymm1,ymm1 4000a6: c5 f4 57 c9 vxorps ymm1,ymm1,ymm1 4000aa: c5 f4 57 c9 vxorps ymm1,ymm1,ymm1 4000ae: c5 f4 57 c9 vxorps ymm1,ymm1,ymm1 4000b2: c5 f4 57 c9 vxorps ymm1,ymm1,ymm1 4000b6: c5 f4 57 c9 vxorps ymm1,ymm1,ymm1 4000ba: 75 e4 jne 4000a0 <_start.loop> 4000bc: 31 ff xor edi,edi 4000be: b8 e7 00 00 00 mov eax,0xe7 4000c3: 0f 05 syscall (ocperf.py is a wrapper with symbolic names for CPU-specific events. It prints the perf command it actually ran): perf stat -etask-clock,cycles,instructions,branches,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xc2,umask=0x2,name=uops_retired_retire_slots/,cpu/event=0xb1,umask=0x1,name=uops_executed_thread/ -r4 ./vxor-zero Performance counter stats for ''./vxor-zero'' (4 runs): 128.379226 task-clock:u (msec) # 0.999 CPUs utilized ( +- 0.07% ) 500,072,741 cycles:u # 3.895 GHz ( +- 0.01% ) 2,000,000,046 instructions:u # 4.00 insn per cycle ( +- 0.00% ) 250,000,040 branches:u # 1947.356 M/sec ( +- 0.00% ) 2,000,012,004 uops_issued_any:u # 15578.938 M/sec ( +- 0.00% ) 2,000,008,576 uops_retired_retire_slots:u # 15578.911 M/sec ( +- 0.00% ) 500,009,692 uops_executed_thread:u # 3894.787 M/sec ( +- 0.00% ) 0.128516502 seconds time elapsed ( +- 0.09% )

El contenido de + - 0.02% se debe a que ejecuté la perf stat -r4 , por lo que ejecuté mi binario 4 veces.

uops_issued_any y uops_retired_retire_slots son dominio fusionado (límite de rendimiento de front-end de 4 por reloj en Skylake y Bulldozer-family). Los recuentos son casi idénticos porque no hay errores imprevistos en las ramas (lo que hace que los uops emitidos especulativamente sean descartados en lugar de retirados).

uops_executed_thread es uops_executed_thread de dominio no fusionado (puertos de ejecución). xor-zeroing no necesita ninguno en las CPU de Intel , por lo que solo se ejecutan los cambios de rumbo y de bifurcación. (Si cambiamos los operandos a vxorps entonces no era solo poner a cero un registro, por ejemplo, vxorps ymm2, ymm1,ymm0 para escribir la salida en un registro que el siguiente no lee, uops ejecutados coincidirá con el uop de dominio fundido contar. Y veríamos que el límite de rendimiento es de tres vxorps por reloj).

2000 millones de uops de dominio fusionado emitidos en ciclos de reloj de 500M son de 4.0 uops emitidos por reloj: alcanzando el rendimiento máximo teórico del front-end. 6 * 250 es 1500, por lo que estos recuentos coinciden con la descodificación de Skylake vxorps ymm,ymm,ymm a 1 uop de dominio fusionado.

Con un número diferente de uops en el bucle, las cosas no son tan buenas. por ejemplo, un bucle de 5 uop solo emitido a 3.75 uops por reloj. Intencionalmente elegí esto para ser 8 uops (cuando vxorps decodifica a un single-uop).

El ancho del problema de Zen es de 6 uops por ciclo, por lo que puede ser mejor con una cantidad diferente de desenrollar. (Consulte esta sección de Preguntas y Respuestas para obtener más información sobre bucles cortos cuyo recuento de uop no es un múltiplo del ancho del problema, en los uarques de la familia Intel SnB).


xor''ing a ymm registrarse en sí mismo genera dos microoperaciones en AMD Ryzen, mientras que xor''ing un registro xmm consigo mismo genera solo una microoperación. Por lo tanto, la forma óptima de hacer xeroing un registro ymm es xor el correspondiente registro xmm consigo mismo y confiar en la extensión cero implícita.

El único procesador que admite AVX512 hoy es Knights Landing. Utiliza una sola microoperación para realizar un registro de zmm. Es muy común manejar una nueva extensión del tamaño del vector dividiéndolo en dos. Esto sucedió con la transición de 64 a 128 bits y con la transición de 128 a 256 bits. Es más que probable que algunos procesadores en el futuro (de AMD o Intel o de cualquier otro proveedor) dividan vectores de 512 bits en dos vectores de 256 bits o incluso cuatro vectores de 128 bits. Por lo tanto, la forma óptima de poner a cero un registro de zmm es sincronizar el registro de 128 bits consigo mismo y confiar en la extensión cero. Y tiene razón, la instrucción con codificación VEX de 128 bits es uno o dos bytes más corta.

La mayoría de los procesadores reconocen que el xor de un registro consigo mismo es independiente del valor anterior del registro.