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


Este bucle se ejecuta en una iteración por 3 ciclos en Intel Conroe/Merom, embotellado en imul rendimiento como se esperaba. Pero en Haswell / Skylake, se ejecuta en una iteración por 11 ciclos, aparentemente porque setnz al tiene una dependencia 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 al depende de rax, la secuencia 3ximul/setcc/movzx forma una cadena de dependencias en bucle. Si no, cada uno setcc/movzx/3ximul cadena es independiente, bifurcada de la dec que actualiza el contador de bucle. El 11c por la iteración medida en HSW / SKL se explica perfectamente por un cuello de botella de latencia: 3x3c (imul) + 1c(lectura-modificación-escritura por setcc) + 1c(movzx dentro del mismo registro).


Off topic: evitar estos cuellos de botella (intencionales)

Iba por un comportamiento comprensible / predecible para aislar cosas de reg parcial, no un rendimiento óptimo.

Por ejemplo, xor-zero / set-flags / setcc es mejor de todos modos (en este caso, xor eax,eax / dec ecx / setnz al). Que se rompe el dep en eax en todas las CPU (excepto la familia P6 temprana como PII y PIII), todavía evita penalizaciones de fusión de registros parciales, y ahorra 1c de latencia movzx. También usa una uop de ALU menos en las CPU que manejan xor-zeroing en la etapa register-rename. Consulte ese enlace para obtener más información sobre el uso de xor-zeroing con setcc.

Tenga en cuenta que AMD, Intel Silvermont/KNL, y P4, no hacen el cambio de nombre de registro parcial en absoluto. Es solo una característica de las CPU de la familia Intel P6 y su descendiente, Intel Sandybridge-familia, pero parece estar siendo eliminado.

Gcc desafortunadamente tiende a usar cmp / setcc al / movzx eax,al donde podría haber utilizado xor en lugar de movzx (Godbolt compiler-explorer example), mientras que clang usa xor-zero/cmp/setcc a menos que combine múltiples condiciones booleanas como count += (a==b) | (a==~b).

La versión xor/dec/setnz se ejecuta a 3.0 c por iteración en Skylake, Haswell y Core2 (embotellado en imul rendimiento). xor - la puesta a cero rompe la dependencia de el antiguo valor de eax en todas las CPU fuera de orden que no sean PPro/PII/PIII/early-Pentium-M (donde todavía evita penalizaciones de fusión de registros parciales pero no rompe el dep). La guía de microarquitectura de Agner Fog describe esto. Reemplazar el cero xor con mov eax,0 lo ralentiza a uno por 4.78 ciclos en Core2: 2-3c stall (en el front-end?) para insertar un uop de fusión de reg parcial cuando imul se lee eax después de setnz al.

También, usé movzx eax, al que vence mov-eliminación, al igual que mov rax,rax hace. (IvB, HSW y SKL pueden renombrar movzx eax, bl con 0 latencia, pero Core2 no puede). Esto hace que todo sea igual en Core2 / SKL, excepto por el comportamiento de registro parcial.


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

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

Desafortunadamente no tiene tiempo para hacer pruebas detalladas para cada nuevo uarch para volver a probar las suposiciones, por lo que este cambio en el comportamiento se deslizó a través de las grietas.

Agner describe un uop de fusión que se inserta (sin estancamiento) 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 gratis. Hojeé la sección Haswell de Agner demasiado rápido, y no noté el párrafo posterior sobre los registros high8. Hazme saber si ves mis comentarios incorrectos en otras publicaciones, para que pueda eliminarlos o agregar una corrección. Intentaré al menos encontrar y editar mis respuestas donde he dicho esto.)


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

Es todo lo mismo de IvyBridge a Skylake, incluyendo la latencia extra high8?

El manual de optimización de Intel no es específico sobre qué CPU tienen dependencias falsas para qué (aunque menciona que algunas CPU las tienen), y omite cosas como leer AH/BH/CH/DH (registros high8) agregando latencia adicional incluso cuando no se han modificado.

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


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

A menos que se indique lo contrario, cada instrucción se ejecuta como 1 uop de dominio fusionado, utilizando una ALU puerto de ejecución. (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 de bucle ocupa parte del ancho de banda del front-end, pero cualquier cosa mejor que 1 por ciclo es una indicación de que el cambio de nombre de registro evitó la dependencia de salida escritura tras escritura, y que la uop no se maneja internamente como un lectura-modificación-escritura.

Escribir solo en AH : evita que el bucle se ejecute desde el búfer de bucle invertido (también conocido como el Detector de Flujo de bucle (LSD)). Los recuentos para lsd.uops son exactamente 0 en HSW, y diminutos en SKL (alrededor de 1.8 k) y no escalan con el conteo de iteración de bucle. Probablemente esos recuentos son de algún código del núcleo. Cuando los bucles se ejecutan desde el LSD, lsd.uops ~= uops_issued hasta dentro del ruido de medición. Algunos bucles alternan entre LSD o no-LSD (por ejemplo, cuando podrían no caber en la caché de uop si la decodificación comienza en el lugar equivocado), pero no me encontré con eso mientras probaba esto.

  • repetido mov ah, bh y/o mov ah, bl se ejecuta a 4 por ciclo. Se necesita un ALU uop, por lo que no se elimina como mov eax, ebx es.
  • repeated mov ah, [rsi] funciona a 2 por ciclo (cuello de botella de rendimiento de carga).
  • repetido mov ah, 123 funciona a 1 por ciclo. (A dep-breaking xor eax,eax dentro del bucle elimina el cuello de botella.)
  • Repetido setz ah o setc ah funciona a 1 por ciclo. (Un dep-breaking xor eax,eax le permite un cuello de botella en el rendimiento p06 para setcc y la rama de bucle.)

    ¿Por qué escribir ah con una instrucción que normalmente usaría una unidad de ejecución ALU tiene una dependencia falsa en el valor antiguo, mientras que mov r8, r/m8 no lo hace (para reg o memory src)? (¿Y qué pasa con mov r/m8, r8? Seguramente no importa cuál de los dos opcodes uses para movimientos reg-reg.)

  • Repeated add ah, 123 funciona a 1 por ciclo, como previsto.

  • repetido add dh, cl funciona a 1 por ciclo.
  • repetido add dh, dh funciona a 1 por ciclo.
  • repetido add dh, ch funciona a 0.5 por ciclo. Leer [ABCD] H es especial cuando están "limpios" (en este caso, RCX no se ha modificado recientemente en absoluto).

Terminología : Todas ellas dejan AH (o DH) " sucio", es decir, en necesidad de fusionar (con un uop de fusión) cuando se lee el resto del registro (o en algunos otros casos). es decir, que AH es renombrado por separado de RAX, si estoy entendiendo esto correctamente. " limpio " es lo contrario. Hay muchas maneras de limpiar un registro sucio, siendo la más simple inc eax o mov eax, esi.

Escribiendo solo a AL: Estos bucles se ejecutan desde el LSD: uops_issue.any ~= lsd.uops.

  • repetido mov al, bl funciona a 1 por ciclo. Una ruptura ocasional dep xor eax,eax por grupo permite un cuello de botella de ejecución de OOO en el rendimiento de uop, no en la latencia.
  • repetido mov al, [rsi] funciona a 1 por ciclo, como ALU micro-fundido + uop de la carga. (uops_issued = 4G + loop overhead,uops_executed = 8G + loop overhead). Un dep-breaking xor eax,eax antes de un grupo de 4 le deja embotellamiento en 2 cargas por reloj.
  • repetido mov al, 123 funciona a 1 por ciclo.
  • repetido mov al, bh funciona a 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.
  • repetido add dl, ch funciona a 0.5 por ciclo. (1 por 2 ciclos). Leer [ABCD]H aparentemente crea latencia extra para dl.
  • repetido add dl, cl funciona a 1 por ciclo.

Creo que una escritura a un reg bajo-8 se comporta como una mezcla de RMW en el reg completo, como sería add eax, 123, pero no desencadena una fusión si ah está sucio. Por lo tanto (aparte de ignorar AH la fusión) se comporta de la misma manera que en las CPU que no hacen el cambio de nombre de reg parcial en absoluto. Parece que AL nunca se cambia el nombre por separado de RAX?

  • inc al/inc ah los pares pueden correr en paralelo.
  • mov ecx, eax inserta un uop fusionado si ah es "sucio", pero el mov real es renombrado. Esto es lo que Agner Fog describe para IvyBridge y posteriores.
  • repetido movzx eax, ah se ejecuta en uno por 2 ciclos. (Lectura alta-8 registros después de escribir regs completo tiene latencia adicional.)
  • movzx ecx, al tiene cero latencia y no toma un puerto de ejecución en HSW y SKL. (Como lo que Agner Fog describe para IvyBridge, pero dice que HSW no cambia el nombre de movzx).
  • movzx ecx, cl tiene latencia 1c y toma un puerto de ejecución. (mov-elimination nunca funciona para el same,same case, solo entre diferentes registros arquitectónicos.)

    Un bucle que inserta un uop de fusión cada iteración no puede ejecutarse desde el LSD (búfer de bucle)?

No creo que haya nada especial en AL/AH/RAX vs.B*, C*, DL/DH/RDX. He probado algunos con regs parciales en otros registros (a pesar de que estoy mostrando sobre todo AL/AH para la consistencia), y nunca han notado ninguna diferencia.

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


Relacionado: Partial flaglos problemas son diferentes de partial register issues. Ver INC instrucción vs ADD 1: ¿Importa? para algunas cosas súper raras con shr r32,cl (e incluso shr r32,2 en Core2/Nehalem: no lea las banderas de un cambio que no sea por 1).

Ver también Problemas con ADC/SBB e INC/DEC en bucles estrechos en algunas CPU para cosas de bandera parcial en bucles adc.

Author: Peter Cordes, 2017-08-13

1 answers

Otras respuestas bienvenidos a 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 es renombrado por separado de RAX (o r15b de r15). Así que si nunca toca los registros high8 (AH / BH / CH / DH), todo se comporta exactamente como en una CPU sin parcial-cambio de nombre reg (por ejemplo, AMD).

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

Agner (e Intel) dicen que Sandybridge puede requerir una fusión de uop para AL, por lo que probablemente sea renombrado por separado de RAX. Para SnB, El manual de optimización de Intel (sección 3.5.2.4 Paradas parciales del registro) dice

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

  • Después de una escritura a uno de los registros AH, BH, CH o DH y antes de una siguiente lectura de la forma de 2, 4 u 8 bytes del mismo registro. En en estos casos se inserta un micro-op de fusión. La inserción consume una asignación completa el ciclo en el que otros micro-ops no be allocated.

  • Después de un micro-op con un registro de destino de 1 o 2 bytes, que es 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 de la misma registrar. En estos casos el micro-op de fusión es parte del flujo.

Creo que están diciendo que en SnB, add al,bl RMW el RAX completo en lugar de renombrarlo por separado, porque una de las fuentes registers es (parte de) RAX. Mi conjetura es que esto no se aplica para una carga como mov al, [rbx + rax]; rax en un modo de direccionamiento probablemente no cuenta como fuente.

No he probado si high8 uops de fusión todavía tienen que emitir/renombrar por su cuenta en HSW/SKL. Eso haría que el impacto front-end equivaliera a 4 uops (ya que ese es el problema/renombrar el ancho de la tubería).

  • No hay manera de romper una dependencia que involucra AL sin escribir EAX/RAX. xor al,al no ayuda, y tampoco does mov al, 0.
  • movzx ebx, al tiene latencia cero (renombrado), y no necesita ninguna unidad de ejecución. (es decir, mov-elimination funciona en HSW y SKL). Desencadena la fusión de AH si está sucio, que supongo que es necesario para que funcione sin un ALU. Probablemente no sea una coincidencia que Intel eliminara el cambio de nombre de low8 en el mismo uarch que introdujo la eliminación de mov. (La guía de micro-arco de Agner Fog tiene un error aquí, diciendo que los movimientos extendidos a cero no se eliminan en HSW o SKL, solo IvB.)
  • movzx eax, ales no eliminado al renombrar. mov-eliminación en Intel nunca funciona para lo mismo, lo mismo. mov rax,rax tampoco se elimina, a pesar de que no tiene que cero-extender nada. (Aunque no tendría sentido darle soporte de hardware especial, porque es solo un no-op, a diferencia de mov eax,eax). De todos modos, prefiere moverse entre dos registros arquitectónicos separados cuando se extiende a cero, ya sea con un mov de 32 bits o un 8 bits movzx.
  • movzx eax, bxes no eliminado al renombrar en HSW o SKL. Tiene latencia de 1c y utiliza un ALU uop. El manual de optimización de Intel solo menciona la latencia cero para movzx de 8 bits (y señala que movzx r32, high8 nunca se cambia el nombre).

High-8 regs se pueden renombrar por separado del resto del registro, y necesitan fusionar uops.

  • Acceso de solo escritura a ah con mov ah, r8 o mov ah, [mem] cambie el nombre de AH, sin dependencia del valor antiguo. Estas son instrucciones que normalmente no necesitarían un uop de ALU (para la versión de 32 bits).
  • un RMW de AH (como inc ah) lo ensucia.
  • setcc ah depende de lo viejo ah, pero todavía lo ensucia. Creo que mov ah, imm8 es lo mismo, pero no he probado tantos casos de esquina.

    (Inexplicable: un bucle que implica setcc ah a veces puede correr desde el LSD, ver el bucle rcr al final de este post. Tal vez mientras ah esté limpio al final del bucle, puede ¿usar el LSD?).

    Si ah está sucio, setcc ah se fusiona en el renombrado ah, en lugar de forzar una fusión en rax. e. g.%rep 4 (inc al / test ebx,ebx / setcc ah / inc al / inc ah) no genera uops de fusión, y solo se ejecuta en aproximadamente 8.7 c (latencia de 8 inc al ralentizada por conflictos de recursos de las uops para ah. También el inc ah / setcc ah dep chain).

    Creo que lo que está pasando aquí es que setcc r8 siempre se implementa como lectura-modificación-escritura. Intel probablemente decidió que no valía la pena tener una uop de solo escritura setcc para optimizar el caso setcc ah, ya que es muy raro que el código generado por el compilador sea setcc ah. (Pero vea el enlace godbolt en la pregunta: clang4.0 con -m32 lo hará.)

  • La lectura de AX, EAX o RAX desencadena una uop de fusión (que toma el problema de front-end/renombrar el ancho de banda). Probablemente la RAT (Register Allocation Table) rastrea el estado high-8-dirty para la arquitectura R [ABCD]X, e incluso después de que una escritura a AH se retira, los datos de AH son almacenado en un registro físico separado de RAX. Incluso con 256 NOPs entre escribir AH y leer EAX, hay un uop de fusión adicional. (ROB size = 224 en SKL, por lo que esto garantiza que el mov ah, 123 fue retirado). Detectado con contadores de perf uops_issued / executed, que muestran claramente la diferencia.

  • Read-modify-write of AL (e.g. inc al) se fusiona de forma gratuita, como parte de la uop de ALU. (Solo probado con unos pocos uops simples, como add/inc, no div r8 o mul r8). Una vez más, no hay fusión uop se activa incluso si AH está sucio.

  • Solo escritura en EAX / RAX (como lea eax, [rsi + rcx] o xor eax,eax) borra el estado AH-dirty (sin fusionar uop).

  • Solo escritura en AX (mov ax, 1) desencadena una fusión de AH primero. Supongo que en lugar de esta carcasa especial, funciona como cualquier otro RMW de AX / RAX. (TODO: test mov ax, bx, aunque eso no debería ser especial porque no está renombrado.)
  • xor ah,ah tiene una latencia de 1c, no es dep-breaking, y todavía necesita una ejecución portuario.
  • Leer y/o escribir de AL no fuerza una fusión, por lo que AH puede permanecer sucio (y ser utilizado independientemente en una cadena dep separada). (e. g.add ah, cl / add al, dl puede ejecutarse a 1 por reloj (embotellado en agregar latencia).

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

Insertar uops de fusión es un poco como insertar uops de stack-sync para el motor de pila. El manual de optimización de Intel dice que el LSD de SnB no puede ejecutar bucles con coincidencias push/pop, lo cual tiene sentido, pero implica que puede ejecutar bucles con balanceado push/pop. Eso no es lo que estoy viendo en SKL: incluso equilibrado push/pop evita correr desde el LSD (p. ej. 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 high-8 está sucio, o cuando contiene uops de motor de pila.

Este comportamiento puede estar relacionado con un una errata en SKL:

SKL150: Los Bucles Cortos Que Usan Registros AH / BH / CH / DH Pueden Causar Un Sistema Impredecible Comportamiento

Problema: Bajo condiciones microarquitectónicas complejas, bucles cortos de menos de 64 instrucciones que usan registros AH, BH, CH o DH, así como sus correspondientes registros más amplios (por ejemplo, RAX, EAX o AX para AH) pueden causar un comportamiento impredecible del sistema. Esto solo puede suceder cuando ambos procesadores lógicos en el mismo procesador físico están activos.

Esto también puede estar relacionado con la declaración del manual de optimización de Intel de que SnB al menos tiene que emita / cambie el nombre de una uop AH-merge en un ciclo por sí misma. Esa es una diferencia extraña para el front-end.

Mi registro del kernel de Linux dice microcode: sig=0x506e3, pf=0x2, revision=0x84. El paquete intel-ucode de Arch Linux solo proporciona la actualización, debe editar los archivos de configuración para que realmente se cargue. Así que mi prueba de Skylake fue en un i7-6700k con la revisión de microcódigo 0x84, que no incluye la solución para SKL150. Coincide con el comportamiento de Haswell en todos los casos que probé, IIRC. (por ejemplo, tanto Haswell como mi SKL puede ejecutar el setne ah / add ah,ah / rcr ebx,1 / mov eax,ebx bucle del LSD). He habilitado HT (que es una condición previa para que se manifieste SKL150), pero estaba probando en un sistema en su mayoría inactivo, por lo que mi hilo tenía el núcleo para sí mismo.

Con el microcódigo actualizado, el LSD está completamente desactivado para todo todo el tiempo, no solo cuando los registros parciales están activos. lsd.uops es siempre exactamente cero, incluso para programas reales no bucles sintéticos. Errores de hardware (en lugar de errores de microcódigo) a menudo requiere deshabilitar una característica completa para corregirla. Esta es la razón por la que SKL-avx512 (SKX) es informado de que no tiene un búfer de bucle invertido. Afortunadamente esto no es un problema de rendimiento: el aumento del rendimiento de la caché uop de SKL sobre Broadwell casi siempre puede mantenerse al día con el problema/cambio de nombre.


Latencia extra AH/BH/CH/DH:

  • Leer AH cuando no está sucio (renombrado por separado) agrega un ciclo adicional de latencia para ambos operandos. por ejemplo, add bl, ah tiene una latencia de 2c desde la entrada BL hasta salida BL, por lo que puede agregar latencia a la ruta crítica incluso si RAX y AH no forman parte de ella. (He visto este tipo de latencia adicional para el otro operando antes, con latencia vectorial en Skylake, donde un retardo int/float "contamina" un registro para siempre. Escribe eso.)

Esto significa desempaquetar bytes con movzx ecx, al / movzx edx, ah tiene latencia extra vs.movzx/shr eax,8/movzx, pero aún mejor rendimiento.

  • Leer AH cuando es sucio no añade cualquier latencia. (add ah,ah o add ah,dh/add dh,ah tener una latencia de 1c por adición). No he hecho muchas pruebas para confirmar esto en muchos casos de esquina.

    Hipótesis: un valor high8 sucio se almacena en la parte inferior de un registro físico . Leer un high8 limpio requiere un cambio para extraer bits [15:8], pero leer un high8 sucio solo puede tomar bits [7: 0] de un registro físico como una lectura normal de registro de 8 bits.

La latencia adicional no significa una reducción del rendimiento. Este el programa puede ejecutarse a 1 iter por 2 relojes, a pesar de que todas las instrucciones add tienen una latencia de 2c (de la lectura de DH, que no se modifica.)

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

Algunos cuerpos de bucle de prueba interesantes :

%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

La versión setcc (con el %if 1) tiene latencia de bucle 20c, y se ejecuta desde el LSD a pesar de que tiene setcc ah y 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% )

Inexplicable: corre del LSD, a pesar de que ensucia. (Al menos creo lo hace. TODO: intente agregar algunas instrucciones que hagan algo con eax antes de que mov eax,ebx lo borre.)

Pero con mov ah, bl, se ejecuta en 5.0 c por iteración (imul cuello de botella de rendimiento) en ambos HSW/SKL. (La tienda/recarga comentada también funciona, pero SKL tiene un reenvío de tienda más rápido que HSW, y es de latencia variable...)

 #  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% )

Observe que ya no se ejecuta desde el LSD.

 17
Author: Peter Cordes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-22 18:35:39