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

Este bucle se ejecuta en una iteración por 3 ciclos en Intel Conroe / Merom, embotellado en el rendimiento de imul como se esperaba. Pero en Haswell / Skylake, se ejecuta en una iteración por 11 ciclos, aparentemente porque setnz al tiene una dependencia en el ú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 dependencia transportada por bucle. De lo contrario, cada setcc movzx setcc / movzx / 3x es independiente, se bifurca de la dec que actualiza el contador de bucles. El 11c por iteración medido en HSW / SKL se explica perfectamente por un cuello de botella de latencia: 3x3c (imul) + 1c (read-modify-write por setcc) + 1c (movzx dentro del mismo registro).


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

Buscaba un comportamiento comprensible / predecible para aislar las cosas de la regla parcial, no el rendimiento óptimo.

Por ejemplo, xor zero / set-flags / setcc es mejor de todos modos (en este caso, xor eax,eax / dec ecx / setnz al ). Eso rompe el dep en eax en todas las CPU (excepto PII-familia temprana como PII y PIII), todavía evita penas de fusión de registro parcial y ahorra 1c de latencia movzx . También utiliza un UOP de ALU menos en las CPU que manejan xor-zeroing en la etapa de cambio de nombre de registro . 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 ningún cambio de nombre de registro parcial. Es solo 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 / setcc al / movzx eax,al que podría haber usado xor lugar de movzx (ejemplo de Godbolt compiler-explorer) , mientras que clang usa xor-zero / cmp / setcc a menos que combine múltiples condiciones booleanas como count += (a==b) | (a==~b) count += (a==b) | (a==~b) .

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

Además, utilicé movzx eax, al cual derrota mov-eliminación, al igual que mov rax,rax hace. (IvB, HSW y SKL pueden cambiar el nombre de movzx eax, bl con latencia 0, pero Core2 no puede). Esto hace que todo sea igual en Core2 / SKL, a excepción del comportamiento de registro parcial.


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

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

Desgraciadamente, no tiene tiempo para hacer pruebas detalladas para cada nuevo cálculo para volver a probar los supuestos, por lo que este cambio en el comportamiento se deslizó por las grietas.

Agner describe una inserción de uop que se inserta (sin locking) para los 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 he dicho que Haswell puede fusionar AH de forma gratuita. Pasé por la sección de Haswell de Agner demasiado rápido y no noté el último párrafo sobre los registros de high8. Déjame saber si ves mis comentarios incorrectos en otras publicaciones, por lo que puedo eliminarlos o agregar una corrección. Trataré, al menos, de encontrar y editar mis respuestas donde dije esto).


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

¿Es todo lo mismo desde IvyBridge hasta Skylake, incluida la latencia extra de high8?

El manual de optimización de Intel no especifica qué CPU tienen dependencias falsas para qué (aunque menciona que algunas CPU las tienen), y deja de lado cosas como leer AH / BH / CH / DH (registros de high8) agregando latencia adicional incluso cuando no tienen acceso ha sido modificado

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


Datos de prueba de My Skylake , desde 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 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 de ALU. (Medido con ocperf.py stat -e ...,uops_issued.any,uops_executed.thread ). Esto detecta (ausencia de) mov-eliminación y uops de fusión adicionales.

Los casos “4 por ciclo” son una extrapolación al caso desenrollado infinitamente. Loop overhead ocupa parte del ancho de banda del front-end, pero cualquier valor superior a 1 por ciclo es una indicación de que el registro-cambio de nombre evitó la dependencia de escritura después de la escritura , y que el uop no se maneja internamente como una lectura-modificación -escribir.

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

  • repite mov ah, bh y / o mov ah, bl ejecuta a 4 por ciclo. Se necesita una ALU uop, por lo que no se elimina como mov eax, ebx es.
  • repetido mov ah, [rsi] ejecuta a 2 por ciclo (cuello de botella de rendimiento de carga).
  • repetido mov ah, 123 ejecuta a 1 por ciclo. (Una ruptura de xor eax,eax dentro del bucle elimina el cuello de botella).
  • setz ah repetido o setc ah ejecuta a 1 por ciclo. (Un dep-break xor eax,eax permite un cuello de botella en el rendimiento de setcc para setcc y la bucle 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 anterior, mientras que mov r8, r/m8 no (para reg o memory src)? (¿Y qué pasa con mov r/m8, r8 ? Seguramente no importa cuál de los dos mov r/m8, r8 usas para los movimientos reg -reg?)

  • repite add ah, 123 carreras a 1 por ciclo, como se esperaba.

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

Terminología : todos estos dejan AH (o DH) ” sucio “, es decir, en necesidad de fusión (con uop de fusión) cuando se lee el rest del registro (o en algunos otros casos). es decir, AH se renombra por separado de RAX, si estoy entendiendo esto correctamente. ” limpio ” es lo opuesto. Hay muchas maneras de limpiar un registro sucio, siendo las más simples inc eax o mov eax, esi .

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

  • repetido mov al, bl ejecuta a 1 por ciclo. Un ocasional xor eax,eax por grupo deja caer OOO cuello de botella de ejecución en el rendimiento uop, no latencia.
  • el mov al, [rsi] repetido mov al, [rsi] ejecuta a 1 por ciclo, como ALU micro fusionado + uop de carga. (uops_issued = overhead 4G + loop, uops_executed = 8G + overhead del bucle). Un despegue xor eax,eax antes de un grupo de 4 lo deja atascado en 2 cargas por reloj.
  • mov al, 123 repetido mov al, 123 ejecuta 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.
  • repite add dl, ch ejecuta a 0.5 por ciclo. (1 por 2 ciclos). La lectura de [ABCD] H aparentemente crea latencia adicional para dl .
  • repetido add dl, cl ejecuta a 1 por ciclo.

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

  • inc al / inc ah pares pueden correr en paralelo.
  • mov ecx, eax inserta un uop de fusión si ah está “sucio”, pero se cambia el nombre del mov real. Esto es lo que Agner Fog describe para IvyBridge y posterior.
  • repite movzx eax, ah ejecuta a una por 2 ciclos. (Leer registros de alto 8 después de escribir registros completos tiene latencia adicional).
  • movzx ecx, al tiene latencia cero 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. ( la eliminación de mov nunca funciona para el same,same caso , solo entre diferentes registros arquitectónicos).

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

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

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


Relacionado: los problemas de bandera parcial son diferentes de los problemas de registro parcial. Ver instrucción INC contra ADD 1: ¿Importa? para algunas cosas súper raras con shr r32,cl (e incluso shr r32,2 en Core2 / Nehalem: no lean banderas de un turno que no sean 1).

Consulte también Problemas con ADC / SBB e INC / DEC en bucles ajustados en algunas CPU para elementos de bandera parcial en bucles adc .

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 parcial entre HSW y SKL. En Haswell y Skylake, todo lo que he probado hasta ahora es compatible con este modelo:

AL nunca se renombra por separado de RAX (o r15b de r15). Por lo tanto, si nunca toca los registros de high8 (AH / BH / CH / DH), todo se comporta exactamente como en una CPU sin cambio de nombre de reg parcial (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 + load uop micro fusionado que se ejecuta en p0156, que es una de las pruebas más sólidas de que se está fusionando realmente en cada escritura, y no solo haciendo una doble contabilidad elegante como Agner especuló.

Agner (e Intel) dicen que Sandybridge puede requerir una combinación de uop para AL, por lo que probablemente se renombre por separado de RAX. Para SnB, el manual de optimización de Intel (sección 3.5.2.4 Parcial Register Stalls) dice:

SnB (no necesariamente uarches posteriores) inserta un uop de fusión en los siguientes casos:

  • Después de escribir en uno de los registros AH, BH, CH o DH y antes de seguir la lectura de la forma de 2, 4 u 8 bytes del mismo registro. En estos casos, se inserta una fusión de microoperació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 fusión de microinterruptores es parte del flujo .

Creo que están diciendo que en SnB, add al,bl RMW todo el RAX en lugar de cambiar el nombre por separado, porque uno de los registros de origen es (parte de) RAX. Supongo que esto no se aplica a una carga como mov al, [rbx + rax] ; rax en un modo de direccionamiento probablemente no cuenta como fuente.

No he probado si high8 merging uops todavía tiene que emitir / cambiar el nombre por su cuenta en HSW / SKL. Eso haría que el impacto del front-end equivalga a 4 uops (ya que ese es el problema / cambio de nombre del ancho de la tubería).

  • No hay forma de romper una dependencia que involucre a AL sin escribir EAX / RAX. xor al,al no ayuda, y tampoco lo hace mov al, 0 .
  • movzx ebx, al tiene latencia cero (renombrado) y no necesita unidad de ejecución. (es decir, la eliminación de mov funciona en HSW y SKL). Activa la fusión de AH si está sucio , lo que supongo es necesario para que funcione sin una ALU. Probablemente no sea una coincidencia que Intel haya bajado el renombrado bajo8 en el mismo uarch que introdujo la eliminación de mov. (La guía de microarcos de Agner Fog tiene un error aquí, diciendo que los movimientos de extensión cero no se eliminan en HSW o SKL, solo en IvB).
  • movzx eax, al no se elimina al cambiar el nombre. 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 extender cero nada. (Aunque no tiene sentido darle soporte de hardware especial, porque no es nada, a diferencia de mov eax,eax ). De todos modos, prefiera moverse entre dos registros arquitectónicos separados cuando se extiende por cero, ya sea con un mov 32 bits o un movzx 8 bits.
  • movzx eax, bx no se elimina al cambiar el nombre en HSW o SKL. Tiene latencia 1c y utiliza un uop de ALU. El manual de optimización de Intel solo menciona latencia cero para movzx de 8 bits (y señala que movzx r32, high8 nunca se renombra).

Los regs High-8 pueden renombrarse por separado del rest del registro, y es necesario fusionar uops.

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

    (Inexplicable: un ciclo que involucra a setcc ah veces puede ejecutarse desde el LSD, vea el bucle rcr al final de esta publicación. ¿Tal vez mientras ah esté limpio al final del ciclo, puede usar el LSD?).

    Si ah está sucio, setcc ah fusiona con el renombrado ah , en lugar de forzar una fusión en rax . ej. %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.7c (latencia de 8 inc al ralentizado por conflictos de recursos de los uops para ah También el inc ah / setcc ah cadena de setcc ah ).

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

  • la lectura de AX, EAX o RAX desencadena una uop de fusión (que ocupa el ancho de banda de emisión / cambio de nombre del front-end). Probablemente, la RAT (Register Allocation Table) rastrea el estado high-8-dirty para la architecture R [ABCD] X, e incluso después de que una escritura en AH se retira, los datos 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 uop. (Tamaño ROB = 224 en SKL, entonces esto garantiza que el mov ah, 123 fue retirado). Detectado con contadores de rendimiento uops emitidos / ejecutados, que muestran claramente la diferencia.

  • Read-modify-write de AL (por ej., inc al ) se fusiona de forma gratuita, como parte de ALU uop. (Solo probado con unos pocos uops simples, como add / inc , no div r8 o mul r8 ). Nuevamente, no se desencadena la fusión uop incluso si AH está sucio.

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

  • Escribir solo para AX ( mov ax, 1 ) desencadena una fusión de AH primero. Supongo que en lugar de tener una carcasa especial, funciona como cualquier otro RMW de AX / RAX. (TODO: prueba mov ax, bx , aunque eso no debería ser especial porque no se renombra).
  • xor ah,ah tiene latencia 1c, no está dep-dep, y todavía necesita un puerto de ejecución.
  • Leer y / o escribir sobre AL no obliga a una fusión, por lo que AH puede mantenerse sucia (y usarse de forma independiente en una cadena de depósito separada). (por ejemplo, add ah, cl / add al, dl puede ejecutarse a 1 por reloj (embotellado en latencia de adición).

Hacer AH sucio impide que se ejecute un bucle 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 emisión / cambio de nombre. (Llamado el IDQ).

Insertar uops de fusión es un poco como insertar uops de sincronización de stack para el stack-engine. El manual de optimización de Intel dice que el LSD de SnB no puede ejecutar bucles con push / pop no coincidentes, lo que tiene sentido, pero implica que puede ejecutar bucles con push / pop balanceado. Eso no es lo que estoy viendo en SKL: incluso el push / pop balanceado evita el funcionamiento del LSD (por ejemplo, push rax / pop rdx / times 6 imul rax, rdx . (Puede haber una diferencia real entre LSD y HSW / SKL de SnB: SnB puede simplemente “bloquear” los uops en el IDQ en lugar de repetirlos varias veces, por lo que un bucle 5-uop tarda 2 ciclos en emitir en lugar de 1.25 ). De todos modos, parece que HSW / SKL no puede usar el LSD cuando un registro de alto 8 está sucio, o cuando contiene uops de motor de stack.

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

Problema: en condiciones complejas de microarchitecture, los bucles cortos de menos de 64 instrucciones que usan registros AH, BH, CH o DH, así como sus registros más amplios correspondientes (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 statement manual de optimización de Intel de que SnB al menos tiene que emitir / cambiar el nombre de una uop de combinación de AH en un ciclo por sí mismo. Esa es una diferencia extraña para el front-end.

Mi registro de kernel de Linux dice microcode: sig=0x506e3, pf=0x2, revision=0x84 . El paquete intel-ucode Arch Linux solo proporciona la actualización, debe editar los archivos de configuración para que realmente se carguen . Así que mi prueba de Skylake fue en un i7-6700k con revisión de microcódigo 0x84, que no incluye la solución para SKL150 . Concuerda con el comportamiento de Haswell en todos los casos que probé, IIRC. (Por ejemplo, tanto Haswell como mi SKL pueden ejecutar el setne ah / add ah,ah / rcr ebx,1 / mov eax,ebx bucle mov eax,ebx del LSD). Tengo 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 en sí mismo.

Con el microcódigo actualizado, el LSD está completamente deshabilitado todo el tiempo, no solo cuando los registros parciales están activos. lsd.uops siempre es exactamente cero, incluso para progtwigs reales, no bucles sintéticos. Los errores de hardware (en lugar de errores de microcódigo) a menudo requieren la desactivación de una característica completa para corregir. Es por esto que se informa que SKL-avx512 (SKX) no tiene un búfer de bucle invertido . Afortunadamente, este no es un problema de rendimiento: el aumento del rendimiento de caché de uops de SKL en Broadwell casi siempre puede estar al día con el problema / cambio de nombre.


Latencia adicional AH / BH / CH / DH:

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

Esto significa desempacar 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 está sucio no agrega ninguna latencia. ( add ah,ah o add ah,dh / add dh,ah tenga 1c de latencia por adición). No he hecho muchas pruebas para confirmar esto en muchos casos de esquina.

    Hipótesis: un valor sucio high8 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 un registro normal de 8 bits leídos.

Latencia adicional no significa rendimiento reducido. Este progtwig puede ejecutarse a 1 iter por 2 relojes, aunque todas las instrucciones add tienen latencia 2c (a partir 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 de setcc (con %if 1 ) tiene una latencia de bucle transportado de 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: funciona desde el LSD, aunque hace que AH esté sucio. (Al menos creo que sí. TODO: intente agregar algunas instrucciones que hagan algo con eax antes del mov eax,ebx borra).

Pero con mov ah, bl , se ejecuta en 5.0c por iteración (cuello de botella de rendimiento de imul ) 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 tiene 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% ) 

Tenga en cuenta que ya no se ejecuta desde el LSD.