¿Cómo funcionan exactamente los registros parciales en Haswell/Skylake? Escribir AL parece tener una falsa dependencia de RAX y AH es inconsistente

Resuelto Peter Cordes asked hace 7 años • 2 respuestas

Este bucle se ejecuta en una iteración cada 3 ciclos en Intel Conroe/Merom, con un cuello de botella en imulel rendimiento como se esperaba. Pero en Haswell/Skylake, se ejecuta en una iteración cada 11 ciclos, aparentemente porque setnz aldepende del último imul.

; synthetic micro-benchmark to test partial-register renaming
    mov     ecx, 1000000000
.loop:                 ; do{
    imul    eax, eax     ; a dep chain with high latency but also high throughput
    imul    eax, eax
    imul    eax, eax

    dec     ecx          ; set ZF, independent of old ZF.  (Use sub ecx,1 on Silvermont/KNL or P4)
    setnz   al           ; ****** Does this depend on RAX as well as ZF?
    movzx   eax, al
    jnz  .loop         ; }while(ecx);

Si setnz aldepende de rax, la secuencia 3ximul/setcc/movzx forma una cadena de dependencia llevada por bucle. De lo contrario, cada cadena setcc/ movzx/3x imules independiente y se bifurca de la decque actualiza el contador de bucle. Los 11c por iteración medidos en HSW/SKL se explican perfectamente por un cuello de botella de latencia: 3x3c(imul) + 1c(lectura-modificación-escritura por setcc) + 1c(movzx dentro del mismo registro).


Fuera de tema: evitar estos cuellos de botella (intencionales)

Buscaba un comportamiento comprensible/predecible para aislar elementos de registro parcial, no un rendimiento óptimo.

Por ejemplo, xor-zero / set-flags / setcces mejor de todos modos (en este caso, xor eax,eax// dec ecx) setnz al. Esto rompe la dependencia de eax en todas las CPU (excepto las primeras familias P6 como PII y PIII), aún evita penalizaciones por fusión de registros parciales y ahorra 1c de movzxlatencia. También utiliza una ALU uop menos en las CPU que manejan la puesta a cero xor en la etapa de cambio de nombre de registro . Consulte ese enlace para obtener más información sobre el uso de xor-zeroing consetcc .

Tenga en cuenta que AMD, Intel Silvermont/KNL y P4 no realizan ningún cambio de nombre de registro parcial. Es sólo una característica de las CPU de la familia Intel P6 y su descendiente, la familia Intel Sandybridge, pero parece que se está eliminando gradualmente.

Desafortunadamente , gcc tiende a usar cmp// donde podría haberse usado en lugar de (ejemplo del compilador-explorador de Godbolt) , mientras que clang usa xor-zero/cmp/setcc a menos que combine múltiples condiciones booleanas comosetcc almovzx eax,alxormovzx count += (a==b) | (a==~b) .

La versión xor/dec/setnz se ejecuta a 3.0c por iteración en Skylake, Haswell y Core2 (con cuellos de botella en imulel rendimiento). xor-La puesta a cero rompe la dependencia del valor anterior de eaxtodas las CPU fuera de servicio que no sean PPro/PII/PIII/early-Pentium-M (donde aún evita penalizaciones por fusión de registros parciales pero no rompe el depósito). La guía de microarcos de Agner Fog describe esto . Reemplazar xor-zeroing con mov eax,0lo ralentiza a uno cada 4,78 ciclos en Core2: parada 2-3c (¿en el front-end?) para insertar un uop de fusión de registro parcial cuando imulse lee eaxdespuéssetnz al .

Además, usé movzx eax, alel método que anula la eliminación de movimientos, al igual que mov rax,raxlo hace. (IvB, HSW y SKL pueden cambiar el nombre movzx eax, blcon latencia 0, pero Core2 no). Esto hace que todo sea igual en Core2/SKL, excepto el comportamiento de registro parcial.


El comportamiento de Core2 es consistente con la guía de microarcos de Agner Fog , pero el comportamiento de HSW/SKL no lo es. De la sección 11.10 para Skylake, y lo mismo para las versiones anteriores de Intel:

Se pueden almacenar diferentes partes de un registro de propósito general en diferentes registros temporales para eliminar dependencias falsas.

Desafortunadamente, no tiene tiempo para realizar pruebas detalladas para cada nuevo uarch para volver a probar las suposiciones, por lo que este cambio de comportamiento pasó desapercibido.

Agner describe la inserción de un uop fusionado (sin detenerse) para registros high8 (AH/BH/CH/DH) en Sandybridge a través de Skylake, y para low8/low16 en SnB. (Desafortunadamente, he estado difundiendo información errónea en el pasado y diciendo que Haswell puede fusionar AH de forma gratuita. Leí la sección Haswell de Agner demasiado rápido y no noté el párrafo posterior sobre los registros high8. Avíseme si ve mis comentarios incorrectos en otras publicaciones, para poder eliminarlos o agregar una corrección. Intentaré al menos encontrar y editar mis respuestas donde dije esto).


Mis preguntas reales: ¿Cómo se comportan realmente exactamente los registros parciales en Skylake?

¿Todo es igual desde IvyBridge hasta Skylake, incluida la latencia adicional High8?

Manual de optimización de Intel. no es específico sobre qué CPU tienen dependencias falsas para qué (aunque sí menciona que algunas CPU las tienen) y omite cosas como leer AH/BH/CH/DH (registros altos 8) que agregan latencia adicional incluso cuando tienen No ha sido modificado.

Si hay algún comportamiento de la familia P6 (Core2/Nehalem) que la guía de microarcos de Agner Fog no describe, eso también sería interesante, pero probablemente debería limitar el alcance de esta pregunta solo a la familia Skylake o Sandybridge.


Mis datos de prueba de Skylake , al colocar %rep 4secuencias cortas dentro de un pequeño dec ebp/jnzbucle que ejecuta iteraciones de 100M o 1G. Medí los ciclos con Linux perfde la misma manera que en mi respuesta aquí. , en el mismo hardware (escritorio Skylake i7 6700k).

A menos que se indique lo contrario, cada instrucción se ejecuta como 1 uop de dominio fusionado, utilizando un puerto de ejecución ALU. (Medido con ocperf.py stat -e ...,uops_issued.any,uops_executed.thread). Esto detecta (ausencia de) eliminación de movimientos y uops de fusión adicionales.

Los casos de "4 por ciclo" son una extrapolación al caso infinitamente desenrollado. La sobrecarga del bucle ocupa parte del ancho de banda del front-end, pero cualquier valor mejor que 1 por ciclo es una indicación de que el cambio de nombre de registro evitó la dependencia de salida de escritura tras escritura y que el uop no se maneja internamente como una modificación de lectura. -escribir.

Escribir solo en AH : evita que el bucle se ejecute desde el búfer de bucle invertido (también conocido como Loop Stream Detector (LSD)). Los recuentos lsd.uopsson exactamente 0 en HSW y pequeños en SKL (alrededor de 1,8k) y no escalan con el recuento de iteraciones del bucle. Probablemente esos recuentos provengan de algún código del kernel. Cuando los bucles van desde el LSD lsd.uops ~= uops_issuedhasta el ruido de medición. Algunos bucles alternan entre LSD y no LSD (por ejemplo, cuando es posible que no quepan en el caché de uop si la decodificación comienza en el lugar equivocado), pero no me encontré con eso mientras probaba esto.

  • repetidos mov ah, bhy/o mov ah, blejecuciones a 4 por ciclo. Se necesita un ALU uop, por lo que no se elimina como mov eax, ebxestá.
  • ejecuciones repetidas mov ah, [rsi]a 2 por ciclo (cuello de botella en el rendimiento de carga).
  • ejecuciones repetidas mov ah, 123a razón de 1 por ciclo. (Una ruptura profundaxor eax,eax dentro del bucle elimina el cuello de botella).
  • Se repite setz aho setc ahse ejecuta a 1 por ciclo. (Una ruptura de profundidad xor eax,eaxpermite que se produzca un cuello de botella en el rendimiento de p06 setccy en la bifurcación del bucle).

    ¿Por qué escribir ahcon una instrucción que normalmente usaría una unidad de ejecución ALU tiene una dependencia falsa del valor anterior, mientras que mov r8, r/m8no la tiene (para reg o src de memoria)? (¿Y qué pasa mov r/m8, r8? Seguramente no importa cuál de los dos códigos de operación uses para los movimientos reg-reg?)

  • ejecuciones repetidas add ah, 123a 1 por ciclo, como se esperaba.

  • ejecuciones repetidas add dh, cla razón de 1 por ciclo.
  • ejecuciones repetidas add dh, dha razón de 1 por ciclo.
  • ejecuciones repetidas add dh, cha 0,5 por ciclo. Leer [ABCD]H es especial cuando están "limpios" (en este caso, RCX no se modificó recientemente en absoluto).

Terminología : Todos estos dejan AH (o DH) " sucio ", es decir, con la necesidad de fusionarse (con un uop de fusión) cuando se lee el resto del registro (o en algunos otros casos). es decir, se cambia el nombre de AH por separado de RAX, si lo entiendo correctamente. " limpio " es todo lo contrario. Hay muchas formas de limpiar una caja registradora sucia, siendo la más sencilla inc eaxo mov eax, esi.

Escribir solo en AL : estos bucles se ejecutan desde el LSD: uops_issue.any~= lsd.uops.

  • ejecuciones repetidas mov al, bla razón de 1 por ciclo. Una interrupción ocasional del depósito xor eax,eaxpor grupo permite que la ejecución de OOO produzca un cuello de botella en el rendimiento de uop, no en la latencia.
  • ejecuciones repetidas mov al, [rsi]a 1 por ciclo, como ALU microfusionado + uop de carga. (uops_issued=4G + sobrecarga de bucle, uops_executed=8G + sobrecarga de bucle). Una ruptura de profundidad xor eax,eaxantes de un grupo de 4 permite que se produzca un cuello de botella en 2 cargas por reloj.
  • ejecuciones repetidas mov al, 123a razón de 1 por ciclo.
  • ejecuciones repetidas mov al, bha 0,5 por ciclo. (1 por 2 ciclos). Leer [ABCD]H es especial.
  • xor eax,eax+ 6x mov al,bh+dec ebp/jnz : 2c por iter, cuello de botella en 4 uops por reloj para el front-end.
  • ejecuciones repetidas add dl, cha 0,5 por ciclo. (1 por 2 ciclos). Leer [ABCD]H aparentemente crea una latencia adicional paradl .
  • ejecuciones repetidas add dl, cla razón de 1 por ciclo.

Creo que una escritura en un registro bajo de 8 se comporta como una combinación RMW en el registro completo, como add eax, 123sería, pero no activa una combinación si ahestá sucia. Entonces (aparte de ignorar AHla fusión) se comporta de la misma manera que en las CPU que no realizan ningún cambio de nombre de registro parcial. Parece que ALnunca se le cambia el nombre por separado de RAX?

  • inc al/ inc ahlos pares pueden ejecutarse en paralelo.
  • mov ecx, eaxinserta un uop fusionado si ahestá "sucio", pero movse cambia el nombre del actual. Esto es lo que describe Agner Fog para IvyBridge y posteriores.
  • movzx eax, ahejecuciones repetidas a una cada 2 ciclos. (Leer registros altos 8 después de escribir registros completos tiene latencia adicional).
  • movzx ecx, altiene latencia cero y no requiere un puerto de ejecución en HSW y SKL. (Como lo que describe Agner Fog para IvyBridge, pero dice que HSW no cambia el nombre de movzx).
  • movzx ecx, cltiene latencia 1c y toma un puerto de ejecución. ( la eliminación de movimientos nunca funciona para el same,samecaso , solo entre diferentes registros arquitectónicos).

    ¿Un bucle que inserta un uop fusionado en cada iteración no se puede ejecutar desde el LSD (búfer de bucle)?

No creo que haya nada especial entre AL/AH/RAX vs. B*, C*, DL/DH/RDX. He probado algunos con registros parciales en otros registros (aunque principalmente estoy mostrando AL/ AHpor coherencia) y nunca he notado ninguna diferencia.

¿Cómo podemos explicar todas estas observaciones con un modelo sensato de cómo funciona internamente el microarco?


Relacionado: Los problemas de banderas parciales son diferentes de los problemas de registros parciales . Consulte la instrucción INC frente a ADD 1: ¿Importa? para algunas cosas súper raras con shr r32,cl(e inclusoshr r32,2 en Core2/Nehalem: no lea banderas de un turno que no sea 1).

Consulte también Problemas con ADC/SBB e INC/DEC en bucles cerrados en algunas CPU para temas de bandera parcial en adcbucles.

Peter Cordes avatar Aug 13 '17 19:08 Peter Cordes
Aceptado

Otras respuestas son bienvenidas para abordar Sandybridge e IvyBridge con más detalle. No tengo acceso a ese hardware.


No he encontrado ninguna diferencia de comportamiento de registro parcial entre HSW y SKL. En Haswell y Skylake, todo lo que he probado hasta ahora es compatible con este modelo:

AL nunca cambia de nombre por separado de RAX (o r15b de r15). Entonces, si nunca tocas los registros altos 8 (AH/BH/CH/DH), todo se comporta exactamente como en una CPU sin cambio de nombre de registro parcial (por ejemplo, AMD).

El acceso de solo escritura a AL se fusiona con RAX, con dependencia de RAX. Para cargas en AL, este es un uop de carga ALU+ microfusionado que se ejecuta en p0156, que es una de las pruebas más sólidas de que realmente se está fusionando en cada escritura, y no solo haciendo una doble contabilidad elegante como especuló Agner.

Agner (e Intel) dicen que Sandybridge puede requerir una fusión para AL, por lo que probablemente se le cambie el nombre por separado de RAX. Para SnB, el manual de optimización de Intel (sección 3.5.2.4 Bloqueos parciales del registro) dice

SnB (no necesariamente uarches posteriores) inserta un uop fusionado en los siguientes casos:

  • Después de escribir en uno de los registros AH, BH, CH o DH y antes de la siguiente lectura del formato de 2, 4 u 8 bytes del mismo registro. En estos casos se inserta una microoperación de fusión. La inserción consume un ciclo de asignación completo en el que no se pueden asignar otras microoperaciones.

  • Después de una microoperación con un registro de destino de 1 o 2 bytes, que no es una fuente de la instrucción (o la forma más grande del registro), y antes de una lectura siguiente de una forma de 2, 4 u 8 bytes del mismo registro. En estos casos, la microoperación de fusión es parte del flujo .

Creo que están diciendo que en SnB, add al,blRMW hará el RAX completo en lugar de cambiarle el nombre por separado, porque uno de los registros fuente es (parte de) RAX. Supongo que esto no se aplica a una carga como mov al, [rbx + rax]; raxen modo de direccionamiento probablemente no cuente como fuente.

No he probado si los uops de fusión de High8 todavía tienen que emitir/cambiar el nombre por sí solos en HSW/SKL. Eso haría que el impacto frontal sea equivalente a 4 uops (ya que ese es el ancho de la tubería de emisión/cambio de nombre).

  • No hay forma de romper una dependencia que involucre AL sin escribir EAX/RAX. xor al,alNo ayuda, y tampoco mov al, 0.
  • movzx ebx, altiene latencia cero (renombrado) y no necesita unidad de ejecución. (es decir, trabajos de eliminación de movimientos en HSW y SKL). Activa la fusión de AH si está sucio , lo cual supongo que es necesario para que funcione sin una ALU. Probablemente no sea una coincidencia que Intel haya abandonado el cambio de nombre de low8 en el mismo uarch que introdujo la eliminación de movimientos. (La guía de microarcos de Agner Fog tiene un error aquí al decir que los movimientos extendidos a cero no se eliminan en HSW o SKL, solo IvB).
  • movzx eax, alno se elimina al cambiar el nombre. La eliminación de movimientos en Intel nunca funciona para lo mismo. mov rax,raxtampoco se elimina, aunque no tiene que extender nada a cero. (Aunque no tendría sentido darle soporte de hardware especial, porque simplemente no es operativo, a diferencia de mov eax,eax). De todos modos, prefiera moverse entre dos registros arquitectónicos separados al realizar la extensión cero, ya sea con un archivo de 32 bits movo de 8 bits movzx.
  • movzx eax, bxno se elimina al cambiar el nombre en HSW o SKL. Tiene latencia 1c y utiliza ALU uop. El manual de optimización de Intel sólo menciona la latencia cero para movzx de 8 bits (y señala que movzx r32, high8nunca se le cambia el nombre).

Los registros High-8 se pueden cambiar de nombre por separado del resto del registro y es necesario fusionarlos.

  • Acceso de solo escritura ahcon AH mov ah, reg8o mov ah, [mem8]cambio de nombre, sin dependencia del valor anterior, a diferencia de mov-immediate. Ambas son instrucciones que normalmente no necesitarían una ALU uop para la versión de 32 bits. (Pero nomov ah, bl se elimina; necesita una ALU uop p0156, por lo que podría ser una coincidencia).
  • un RMW de AH (como inc ah) lo ensucia (por lo que se le cambia el nombre por separado y necesita una fusión uop si se lee AX, EAX o RAX más tarde, incluso como parte de la escritura de AX).
  • setcc ahDepende del viejo ah, pero aún así lo ensucia. Creo que mov ah, imm8es lo mismo, pero no he probado tantos casos extremos.

(Inexplicable: un bucle que involucra setcc aha veces puede ejecutarse desde el LSD, vea el rcrbucle al final de esta publicación. Tal vez, siempre que ahesté limpio al final del bucle, ¿puede usar el LSD?).

Si ahestá sucio, setcc ahse fusiona con el archivo renombrado ah, en lugar de forzar una fusión con el archivo rax. por ejemplo %rep 4( inc al/ test ebx,ebx/ setcc ah/ inc al/ inc ah) no genera uops fusionados y solo se ejecuta en aproximadamente 8.7c (latencia de 8 inc alralentizada por conflictos de recursos de los uops para ah. También la cadena inc ah/ setcc ahdep).

Creo que lo que sucede aquí es que setcc r8siempre se implementa como lectura-modificación-escritura. Intel probablemente decidió que no valía la pena tener un setccuop de solo escritura para optimizar el setcc ahcaso, ya que es muy raro que el código generado por el compilador sea setcc ah. (Pero vea el enlace de Godbolt en la pregunta: clang4.0 -m32lo hará).

  • La lectura de AX, EAX o RAX desencadena una fusión uop (que ocupa el problema de front-end/cambia el nombre del ancho de banda). Probablemente, la RAT (Tabla de asignación de registros) rastrea el estado de alta suciedad de 8 para la arquitectura R[ABCD]X, e incluso después de que se retira una escritura en AH, los datos de AH se almacenan en un registro físico separado de RAX. Incluso con 256 NOP entre escribir AH y leer EAX, hay una fusión adicional. (Tamaño de ROB = 224 en SKL, por lo que esto garantiza que se mov ah, 123retiró). Detectado con contadores de rendimiento uops_issued/executed, que muestran claramente la diferencia.

  • La lectura, modificación y escritura de AL (p. ej. inc al) se fusiona de forma gratuita, como parte de ALU uop. (Solo probado con algunos uops simples, como add/ inc, no div r8o mul r8). Nuevamente, no se activa ninguna fusión uop incluso si AH está sucio.

  • La escritura de solo en EAX/RAX (como lea eax, [rsi + rcx]o xor eax,eax) borra el estado sucio de AH (sin fusionar uop).

  • La escritura de solo en AX ( mov ax, 1o mov ax, bx) activa primero una fusión de AH. Supongo que en lugar de usar una carcasa especial, se ejecuta como cualquier otro RMW de AX/RAX.

  • xor ah,ahtiene una latencia de 1c, no interrumpe el almacenamiento y aún necesita un puerto de ejecución. mov ah,0es el mismo; al igual que con setcc, quizás mov-immediate to 8-bit reg no tenga un formato especial para registros de 8 niveles altos, a diferencia de conmov reg,reg

  • La lectura y/o escritura de AL no fuerza una fusión, por lo que AH puede permanecer sucio (y usarse de forma independiente en una cadena de depósito separada). (por ejemplo, add ah, cl/ add al, dlpuede funcionar a 2 IPC, un par por reloj (cuello de botella al agregar latencia).


Ensuciar AH evita que se ejecute un bucle desde el LSD (el búfer de bucle), incluso cuando no hay fusiones de uops. El LSD es cuando la CPU recicla uops en la cola que alimenta la etapa de emisión/cambio de nombre. (Llamado IDQ).

Insertar uops fusionados es un poco como insertar uops de sincronización de pila para el motor de pila. El manual de optimización de Intel dice que el LSD de SnB no puede ejecutar bucles con push/ no coincidentes pop, lo cual tiene sentido, pero implica que puedepush ejecutar bucles con / equilibrado pop. Eso no es lo que estoy viendo en SKL: incluso equilibrado push/ popevita que se ejecute desde el LSD (por ejemplo, push rax/ pop rdx/ times 6 imul rax, rdx​​. (Puede haber una diferencia real entre el LSD de SnB y HSW/SKL: SnB puede simplemente "bloquear" los uops en el IDQ en lugar de repetirlos varias veces, por lo que un bucle de 5 uop tarda 2 ciclos en emitirse en lugar de 1,25 ). De todos modos, parece que HSW/SKL no puede usar el LSD cuando un registro de 8 altos está sucio o cuando contiene uops del motor de pila.

Este comportamiento puede estar relacionado con una errata en SKL :

SKL150: Los bucles cortos que utilizan registros AH/BH/CH/DH pueden provocar un comportamiento impredecible del sistema

Problem: Under complex micro-architectural conditions, short loops of less than 64 instruction that use AH, BH, CH, or DH registers as well as their corresponding wider registers (e.g. RAX, EAX, or AX for AH) may cause unpredictable system behaviour. This can only happen when both logical processors on the same physical processor are active.

This may also be related to Intel's optimization manual statement that SnB at least has to issue/rename an AH-merge uop in a cycle by itself. That's a weird difference for the front-end.

My Linux kernel log says microcode: sig=0x506e3, pf=0x2, revision=0x84. Arch Linux's intel-ucode package just provides the update, you have to edit config files to actually have it loaded. So my Skylake testing was on an i7-6700k with microcode revision 0x84, which doesn't include the fix for SKL150. It matches the Haswell behaviour in every case I tested, IIRC. (e.g. both Haswell and my SKL can run the setne ah / add ah,ah / rcr ebx,1 / mov eax,ebx loop from the LSD). I have HT enabled (which is a pre-condition for SKL150 to manifest), but I was testing on a mostly-idle system so my thread had the core to itself.

With updated microcode, the LSD is completely disabled for everything all the time, not just when partial registers are active. lsd.uops is always exactly zero, including for real programs not synthetic loops. Hardware bugs (rather than microcode bugs) often require disabling a whole feature to fix. This is why SKL-avx512 (SKX) is reported to not have a loopback buffer. Fortunately this is not a performance problem: SKL's increased uop-cache throughput over Broadwell can almost always keep up with issue/rename.


Extra AH/BH/CH/DH latency:

  • Reading AH when it's not dirty (renamed separately) adds an extra cycle of latency for both operands. e.g. add bl, ah has a latency of 2c from input BL to output BL, so it can add latency to the critical path even if RAX and AH are not part of it. (I've seen this kind of extra latency for the other operand before, with vector latency on Skylake, where an int/float delay "pollutes" a register forever. TODO: write that up.)

This means unpacking bytes with movzx ecx, al / movzx edx, ah has extra latency vs. movzx/shr eax,8/movzx, but still better throughput.

  • Reading AH when it is dirty doesn't add any latency. (add ah,ah or add ah,dh/add dh,ah have 1c latency per add). I haven't done a lot of testing to confirm this in many corner-cases.

Hypothesis: a dirty high8 value is stored in the bottom of a physical register. Reading a clean high8 requires a shift to extract bits [15:8], but reading a dirty high8 can just take bits [7:0] of a physical register like a normal 8-bit register read.

Extra latency doesn't mean reduced throughput. This program can run at 1 iter per 2 clocks, even though all the add instructions have 2c latency (from reading DH, which is not modified.)

global _start
_start:
    mov     ebp, 100000000
.loop:
    add ah, dh
    add bh, dh
    add ch, dh
    add al, dh
    add bl, dh
    add cl, dh
    add dl, dh

    dec ebp
    jnz .loop

    xor edi,edi
    mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
    syscall       ; sys_exit_group(0)
 Performance counter stats for './testloop':

     48.943652      task-clock (msec)         #    0.997 CPUs utilized          
             1      context-switches          #    0.020 K/sec                  
             0      cpu-migrations            #    0.000 K/sec                  
             3      page-faults               #    0.061 K/sec                  
   200,314,806      cycles                    #    4.093 GHz                    
   100,024,930      branches                  # 2043.675 M/sec                  
   900,136,527      instructions              #    4.49  insn per cycle         
   800,219,617      uops_issued_any           # 16349.814 M/sec                 
   800,219,014      uops_executed_thread      # 16349.802 M/sec                 
         1,903      lsd_uops                  #    0.039 M/sec                  

   0.049107358 seconds time elapsed

Some interesting test loop bodies:

%if 1
     imul eax,eax
     mov  dh, al
     inc dh
     inc dh
     inc dh
;     add al, dl
    mov cl,dl
    movzx eax,cl
%endif

Runs at ~2.35c per iteration on both HSW and SKL.  reading `dl` has no dep on the `inc dh` result.  But using `movzx eax, dl` instead of `mov cl,dl` / `movzx eax,cl` causes a partial-register merge, and creates a loop-carried dep chain.  (8c per iteration).


%if 1
    imul  eax, eax
    imul  eax, eax
    imul  eax, eax
    imul  eax, eax
    imul  eax, eax         ; off the critical path unless there's a false dep

  %if 1
    test  ebx, ebx          ; independent of the imul results
    ;mov   ah, 123         ; dependent on RAX
    ;mov  eax,0           ; breaks the RAX dependency
    setz  ah              ; dependent on RAX
  %else
    mov   ah, bl          ; dep-breaking
  %endif

    add   ah, ah
    ;; ;inc   eax
;    sbb   eax,eax

    rcr   ebx, 1      ; dep on  add ah,ah  via CF
    mov   eax,ebx     ; clear AH-dirty

    ;; mov   [rdi], ah
    ;; movzx eax, byte [rdi]   ; clear AH-dirty, and remove dep on old value of RAX
    ;; add   ebx, eax          ; make the dep chain through AH loop-carried
%endif

The setcc version (with the %if 1) has 20c loop-carried latency, and runs from the LSD even though it has setcc ah and add ah,ah.

00000000004000e0 <_start.loop>:
  4000e0:       0f af c0                imul   eax,eax
  4000e3:       0f af c0                imul   eax,eax
  4000e6:       0f af c0                imul   eax,eax
  4000e9:       0f af c0                imul   eax,eax
  4000ec:       0f af c0                imul   eax,eax
  4000ef:       85 db                   test   ebx,ebx
  4000f1:       0f 94 d4                sete   ah
  4000f4:       00 e4                   add    ah,ah
  4000f6:       d1 db                   rcr    ebx,1
  4000f8:       89 d8                   mov    eax,ebx
  4000fa:       ff cd                   dec    ebp
  4000fc:       75 e2                   jne    4000e0 <_start.loop>

 Performance counter stats for './testloop' (4 runs):

       4565.851575      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.08% )
                 4      context-switches          #    0.001 K/sec                    ( +-  5.88% )
                 0      cpu-migrations            #    0.000 K/sec                  
                 3      page-faults               #    0.001 K/sec                  
    20,007,739,240      cycles                    #    4.382 GHz                      ( +-  0.00% )
     1,001,181,788      branches                  #  219.276 M/sec                    ( +-  0.00% )
    12,006,455,028      instructions              #    0.60  insn per cycle           ( +-  0.00% )
    13,009,415,501      uops_issued_any           # 2849.286 M/sec                    ( +-  0.00% )
    12,009,592,328      uops_executed_thread      # 2630.307 M/sec                    ( +-  0.00% )
    13,055,852,774      lsd_uops                  # 2859.456 M/sec                    ( +-  0.29% )

       4.565914158 seconds time elapsed                                          ( +-  0.08% )

Unexplained: it runs from the LSD, even though it makes AH dirty. (At least I think it does. TODO: try adding some instructions that do something with eax before the mov eax,ebx clears it.)

But with mov ah, bl, it runs in 5.0c per iteration (imul throughput bottleneck) on both HSW/SKL. (The commented-out store/reload works, too, but SKL has faster store-forwarding than HSW, and it's variable-latency...)

 #  mov ah, bl   version
 5,009,785,393      cycles                    #    4.289 GHz                      ( +-  0.08% )
 1,000,315,930      branches                  #  856.373 M/sec                    ( +-  0.00% )
11,001,728,338      instructions              #    2.20  insn per cycle           ( +-  0.00% )
12,003,003,708      uops_issued_any           # 10275.807 M/sec                   ( +-  0.00% )
11,002,974,066      uops_executed_thread      # 9419.678 M/sec                    ( +-  0.00% )
         1,806      lsd_uops                  #    0.002 M/sec                    ( +-  3.88% )

   1.168238322 seconds time elapsed                                          ( +-  0.33% )

Notice that it doesn't run from the LSD anymore.


2023 update

Some more tests, still on i7-6700k Skylake, this time with newer microcode (version 0xf0) that permanently disabled the LSD (because of AH-merging correctness issue in corner cases, apparently.)

.loop:
   times 6 mov ah, cl   ; or as low as times 3.  But times 7 makes the loop slower, 4c / iter
    mov ax, bx          ; trigger an AH merge
    dec ebp
    jnz .loop
.end:
  • With times 1 or times 2 mov ah, cl: 2 cycles / iter.
    (4 or 5 uops issued and executed / iter)
  • With times 3 to times 6 mov ah, cl: 3 cycles / iter
    (6 to 10 uops issued and executed)
  • With times 7 to times 10 mov ah, cl: 4 cycles / iter

The bottleneck with mov ah, cl is I think front-end throughput. An AH-merging uop needing to issue in a cycle by itself would explain this, e.g. an upper limit of 5 uops in 2 cycles, with 4 in one cycle, 1 in the other. And increasing by 1 cycle for each 4 more uops. That front-end bubble for the AH-merging uop should give the uop cache time to keep up.

Esto también demuestra mov ah, clque no depende del antiguo RAX después de la fusión: si hubiera una dependencia de salida, no podría ejecutarse más rápido que 1 instrucción por ciclo (sin contar dec/jne).

Pero xor ah,ahor mov ah,0crea una cadena de dependencia transmitida por bucle, exactamente como inc ah, lo que confirma que mov ax, bxtiene una dependencia de AH. Limita la velocidad a n+3 ciclos por iteración, donde nestá la longitud de la times n mov ah,0cadena. Por ejemplo, times 7se necesitan 10 ciclos, con 10 uops emitidos y ejecutados. (De 10 instrucciones, incluido un dec/jne fusionado con macro, por lo que hay un uop adicional allí).

Parece que el costo de latencia del uop de fusión de AH es de 1 ciclo, y el mov ax, bxcosto de otro ciclo para fusionar un nuevo valor en la parte inferior de RAX. El ciclo adicional se produce al leer AH después de escribir RAX. Agregar a mov ah, almantiene la dependencia de RAX, pero sin leer AH.

Peter Cordes avatar Aug 13 '2017 12:08 Peter Cordes

Actualización: Posible evidencia de que IvyBridge todavía cambia el nombre de los registros low16/low8 por separado del registro completo, como Sandybridge pero a diferencia de Haswell y posteriores.

Los resultados de InstLatX64 de SnB e IvB muestran un rendimiento de 0,33c movsx r16, r8(como se esperaba, movsxnunca se elimina y solo había 3 ALU antes de Haswell).

Pero aparentemente movsx r16, r8la prueba de InstLat produce cuellos de botella en Haswell / Broadwell / Skylake con un rendimiento de 1c (consulte también este informe de error en instlat github ). Probablemente escribiendo el mismo registro arquitectónico, creando una cadena de fusiones.

(El rendimiento real para esa instrucción con registros de destino separados es 0.25c en mi Skylake. Probado con 7 movsxinstrucciones escritas en eax..edi y r10w/r11w, todas leyendo desde cl. Y a dec ebp/jnzcomo rama del bucle para hacer un bucle par de 8 uop .)

Si adivino correctamente qué creó ese resultado de rendimiento de 1c en las CPU después de IvB, es algo así como ejecutar un bloque de movsx dx, al. Y eso solo puede ejecutarse en más de 1 IPC en CPU que cambian de nombre dxpor separado de RDX en lugar de fusionarse. Entonces podemos concluir que IvB en realidad todavía cambia el nombre de los registros low8/low16 por separado de los registros completos, y no fue hasta Haswell que lo abandonaron. ( Pero hay algo sospechoso aquí: si esta explicación fuera correcta, deberíamos ver el mismo rendimiento de 1c en AMD que no cambia el nombre de registros parciales. Pero no es así, consulte a continuación ).

Resultados con un rendimiento de ~0,33c para las pruebas movsx r16, r8(y movzx r16, r8):

  • IvB con compilación AIDA64: 4.0.568.0 24 de mayo de 2013
  • Compilación IvB-E: 4.3.764.0 10 de julio de 2017
  • SnB-EP con una construcción de 2013
  • SnB con una construcción de 2018 .

Resultados de Haswell con un 0.58crendimiento misterioso para movsx/zx r16, r8:

  • Un resultado de Haswell con la misma compilación 4.3.764.0 del 10 de julio de 2017 de AIDA64
  • Haswell-E con una construcción de 2014

Otros resultados anteriores y posteriores de Haswell (y CrystalWell) / Broadwell / Skylake tienen un rendimiento de 1.0c para esas dos pruebas.

  • HSW con 4.1.570.0 el 5 de junio de 2013, BDW con 4.3.15787.0 el 12 de octubre de 2018, BDW con 4.3.739.0 el 17 de marzo de 2017.

Como informé en el problema vinculado de InstLat en github, los números de "latencia" para movzx r32, r8ignorar la eliminación de movimientos, presumiblemente probando como movzx eax, al.

Peor aún, las versiones más nuevas de InstLatX64 con versiones de la prueba con registros separados, como MOVSX r1_32, r2_8, muestran números de latencia por debajo de 1 ciclo, como 0.3c para ese MOV SX en Skylake. Esto es un completo disparate; Lo probé sólo para estar seguro.

La MOVSX r1_16, r2_8prueba muestra una latencia de 1c, por lo que aparentemente solo están midiendo la latencia de la dependencia de salida (falsa) . (Que no existe para salidas de 32 bits y más anchas).

¡Pero esa prueba también MOVSX r1_16, r2_8midió una latencia de 1c en Sandybridge ! Entonces, tal vez mi teoría estaba equivocada sobre lo que movsx r16, r8nos dice la prueba.


En Ryzen (AIDA64 build 4.3.781.0 21 de febrero de 2018), que sabemos que no realiza ningún cambio de nombre de registro parcial , los resultados no muestran el efecto de rendimiento de 1c que esperaríamos si la prueba realmente escribiera el mismo registro de 16 bits repetidamente. Tampoco lo encuentro en ninguna CPU AMD más antigua, con versiones anteriores de InstLatX64, como K10 o Bulldozer-family.

## Instlat Zen tests of ... something?
  43 X86     :MOVSX r16, r8                L:   0.28ns=  1.0c  T:   0.11ns=  0.40c
  44 X86     :MOVSX r32, r8                L:   0.28ns=  1.0c  T:   0.07ns=  0.25c
  45 AMD64   :MOVSX r64, r8                L:   0.28ns=  1.0c  T:   0.12ns=  0.43c
  46 X86     :MOVSX r32, r16               L:   0.28ns=  1.0c  T:   0.12ns=  0.43c
  47 AMD64   :MOVSX r64, r16               L:   0.28ns=  1.0c  T:   0.13ns=  0.45c
  48 AMD64   :MOVSXD r64, r32              L:   0.28ns=  1.0c  T:   0.13ns=  0.45c

IDK por qué el rendimiento no es 0,25 para todos ellos; Parece raro. Esta podría ser una versión del efecto de rendimiento de Haswell 0.58c. Los números MOVZX son los mismos, con un rendimiento de 0,25 para la versión sin prefijos que lee R8 y escribe R32. ¿Quizás hay un cuello de botella al buscar/decodificar instrucciones más grandes? Pero movsx r32, r16es del mismo tamaño que movsx r32, r8.

Sin embargo, las pruebas de registros separados muestran el mismo patrón que en Intel, con una latencia de 1c solo para el que tiene que fusionarse. MOVZX es lo mismo.

## Instlat Zen separate-reg tests
2252 X86     :MOVSX r1_16, r2_8            L:   0.28ns=  1.0c  T:   0.08ns=  0.28c
2253 X86     :MOVSX r1_32, r2_8            L:   0.07ns=  0.3c  T:   0.07ns=  0.25c
2254 AMD64   :MOVSX r1_64, r2_8            L:   0.07ns=  0.3c  T:   0.07ns=  0.25c
2255 X86     :MOVSX r1_32, r2_16           L:   0.07ns=  0.3c  T:   0.07ns=  0.25c

Los resultados de la excavadora también son bastante similares a este, pero, por supuesto, con un rendimiento menor.

https://www.uops.info/table.html confirma que Zen+ tiene el rendimiento esperado de 0,25c (y una latencia de 1c) MOVSX_NOREX (R16, R8), lo mismo que Instlat encontró con sus pruebas de registro separado.

¿Quizás la prueba de rendimiento de InstLat para MOVSX r16, r8(no MOVSX r1_16, r2_8) solo usa 2 o 3 cadenas dep, lo cual no es suficiente para las CPU modernas? ¿O tal vez rompe la cadena de departamento de vez en cuando para que el ejecutivo de OoO pueda superponerse?

Peter Cordes avatar May 03 '2019 03:05 Peter Cordes