¿Es vxorps-zeroing en AMD Jaguar / Bulldozer / Zen más rápido con registros xmm que ymm?

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

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

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

Las tablas de instrucciones de Agner Fog no enumeran este caso especial, y su guía de microarquitectura no menciona la cantidad de uops.

Esto podría significarvxorps xmm0,xmm0,xmm0 es una mejor manera de implementar_mm256_setzero_ps().

Para AVX512,_mm512_setzero_ps() también guarda un byte usando solo un idioma de reducción a cero codificado por VEX, en lugar de EVEX, cuando es posible. (es decir, para zmm0-15.vxorps xmm31,xmm31,xmm31 aún requeriría un EVEX). Actualmente, gcc / clang usa expresiones idiomáticas xor-zeroing del ancho de registro que deseen, en lugar de usar siempre AVX-128.

Reportado como clangerror 32862 y gccerror 80636. MSVC ya usaxmm. Todavía no se ha informado a ICC, que también utiliza registros zmm para la puesta a cero AVX512. (Aunque a Intel podría no importarle cambiar ya que actualmente no hay ningún beneficio en ninguna CPU de Intel, solo AMD. Si alguna vez lanzan una CPU de baja potencia que divide los vectores a la mitad, podrían hacerlo. Su diseño actual de baja potencia (Silvermont) no funciona No es compatible con AVX, solo SSE4.)

El único inconveniente 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 derrotar a un hack C o C ++ que intenta calentarlos.

(Las instrucciones de vector de 256b son más lentas durante los primeros ~ 56k ciclos después de la primera instrucción de 256b. Consulte la sección de Skylake en el microarchivo de Agner Fog en pdf). Probablemente esté bien si llama a unnoinline función que devuelve_mm256_setzero_ps no es una forma confiable de calentar las unidades de ejecución. (Uno que todavía funciona sin AVX2 y evita cualquier carga (que podría fallar en la memoria caché) es__m128 onebits = _mm_castsi128_ps(_mm_set1_epi8(0xff));
return _mm256_insertf128_ps(_mm256_castps128_ps256(onebits), onebits) que debería compilar apcmpeqd xmm0,xmm0,xmm0 / vinsertf128 ymm0,xmm0,1. Eso sigue siendo bastante trivial para algo que llamas una vez para calentar (o mantener caliente) las unidades de ejecución mucho antes de un ciclo crítico. Y si quieres algo que pueda estar en línea, probablemente necesites inline-asm.)

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

Si alguien tiene hardware AMD pero no sabe cómo probarlo, use contadores de rendimiento para contar los ciclos (y, de preferencia, m-ops u 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, quizás reemplace las cosas después del bucle (la llamada al sistema de salida) con unrety llamar a la función desde una Cmain() función.

Montar connasm -felf64 vxor-zero.asm && ld -o vxor-zero vxor-zero.o hacer un binario estático (O usarelasm-link script que publiqué en un Q&A 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 unos minutos. Turbo a 4.2 o 4.4GHz funciona normalmente justo después del arranque). Como estoy usando contadores de rendimiento, en realidad no importa qué velocidad de reloj esté funcionando la máquina. No hay cargas / almacenes o errores de caché de código involucrados, por lo que el número de ciclos de reloj de núcleo para todo es constante, independientemente de cuánto tiempo sean.

$ 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 + - 0.02% es porque corríperf stat -r4, así que ejecutó mi binario 4 veces.

uops_issued_any yuops_retired_retire_slots son de 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 predicciones erróneas de la rama (lo que lleva a que se descarten uops emitidos especulativamente en lugar de retirarse).

uops_executed_thread es uops de dominio sin fusionar (puertos de ejecución).xor-zeroing no necesita ninguno en las CPU Intel, por lo que solo se ejecutan los dec y branch uops. (Si cambiamos los operandos a vxorps por lo que no se trata solo de poner a cero un registro, p. Ej.vxorps ymm2, ymm1,ymm0 para escribir la salida en un registro que el siguiente no lee, los uops ejecutados coincidirán con el conteo de uops del dominio fusionado. Y veríamos que el límite de rendimiento es de tres vxorps por reloj).

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

Con un número diferente de uops en el ciclo, las cosas no son tan buenas. p.ej. un bucle de 5 uops solo emitido a 3.75 uops por reloj. Elegí intencionalmente que esto fuera 8 uops (cuando vxorps se decodifica en un solo uop).

El ancho de emisión de Zen es de 6 uops por ciclo, por lo que puede funcionar mejor con una cantidad diferente de desenrollado. (Verestas preguntas y respuestas para obtener más información sobre los bucles cortos cuyo recuento de uop no es un múltiplo del ancho del problema, en uarches de la familia Intel SnB).

Respuestas a la pregunta(1)

Su respuesta a la pregunta