¿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 cada 3 ciclos en Intel Conroe/Merom, con un cuello de botella en imul
el rendimiento como se esperaba. Pero en Haswell/Skylake, se ejecuta en una iteración cada 11 ciclos, aparentemente porque setnz al
depende 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 dependencia llevada por bucle. De lo contrario, cada cadena setcc
/ movzx
/3x imul
es independiente y se bifurca de la dec
que 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 / setcc
es 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 movzx
latencia. 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 al
movzx eax,al
xor
movzx
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 imul
el rendimiento). xor
-La puesta a cero rompe la dependencia del valor anterior de eax
todas 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,0
lo 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 imul
se lee eax
despuéssetnz al
.
Además, usé movzx eax, al
el método que anula la eliminación de movimientos, al igual que mov rax,rax
lo hace. (IvB, HSW y SKL pueden cambiar el nombre movzx eax, bl
con 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 4
secuencias cortas dentro de un pequeño dec ebp/jnz
bucle 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 (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.uops
son 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_issued
hasta 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, bh
y/omov ah, bl
ejecuciones a 4 por ciclo. Se necesita un ALU uop, por lo que no se elimina comomov eax, ebx
está. - ejecuciones repetidas
mov ah, [rsi]
a 2 por ciclo (cuello de botella en el rendimiento de carga). - ejecuciones repetidas
mov ah, 123
a razón de 1 por ciclo. (Una ruptura profundaxor eax,eax
dentro del bucle elimina el cuello de botella). Se repite
setz ah
osetc ah
se ejecuta a 1 por ciclo. (Una ruptura de profundidadxor eax,eax
permite que se produzca un cuello de botella en el rendimiento de p06setcc
y en la bifurcación del bucle).¿Por qué escribir
ah
con una instrucción que normalmente usaría una unidad de ejecución ALU tiene una dependencia falsa del valor anterior, mientras quemov r8, r/m8
no la tiene (para reg o src de memoria)? (¿Y qué pasamov 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, 123
a 1 por ciclo, como se esperaba.- ejecuciones repetidas
add dh, cl
a razón de 1 por ciclo. - ejecuciones repetidas
add dh, dh
a razón de 1 por ciclo. - ejecuciones repetidas
add dh, ch
a 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 eax
o mov eax, esi
.
Escribir solo en AL : estos bucles se ejecutan desde el LSD: uops_issue.any
~= lsd.uops
.
- ejecuciones repetidas
mov al, bl
a razón de 1 por ciclo. Una interrupción ocasional del depósitoxor eax,eax
por 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 profundidadxor eax,eax
antes de un grupo de 4 permite que se produzca un cuello de botella en 2 cargas por reloj. - ejecuciones repetidas
mov al, 123
a razón de 1 por ciclo. - ejecuciones repetidas
mov al, bh
a 0,5 por ciclo. (1 por 2 ciclos). Leer [ABCD]H es especial. xor eax,eax
+ 6xmov al,bh
+dec ebp/jnz
: 2c por iter, cuello de botella en 4 uops por reloj para el front-end.- ejecuciones repetidas
add dl, ch
a 0,5 por ciclo. (1 por 2 ciclos). Leer [ABCD]H aparentemente crea una latencia adicional paradl
. - ejecuciones repetidas
add dl, cl
a 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, 123
sería, pero no activa una combinación si ah
está sucia. Entonces (aparte de ignorar AH
la 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 AL
nunca se le cambia el nombre por separado de RAX
?
inc al
/inc ah
los pares pueden ejecutarse en paralelo.mov ecx, eax
inserta un uop fusionado siah
está "sucio", peromov
se cambia el nombre del actual. Esto es lo que describe Agner Fog para IvyBridge y posteriores.movzx eax, ah
ejecuciones repetidas a una cada 2 ciclos. (Leer registros altos 8 después de escribir registros completos tiene latencia adicional).movzx ecx, al
tiene 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, cl
tiene latencia 1c y toma un puerto de ejecución. ( la eliminación de movimientos nunca funciona para elsame,same
caso , 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
/ AH
por 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 adc
bucles.
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,bl
RMW 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]
; rax
en 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,al
No ayuda, y tampocomov al, 0
. movzx ebx, al
tiene 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, al
no se elimina al cambiar el nombre. La eliminación de movimientos en Intel nunca funciona para lo mismo.mov rax,rax
tampoco 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 demov 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 bitsmov
o de 8 bitsmovzx
.movzx eax, bx
no 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 quemovzx r32, high8
nunca 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
ah
con AHmov ah, reg8
omov 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 ah
Depende del viejoah
, pero aún así lo ensucia. Creo quemov ah, imm8
es lo mismo, pero no he probado tantos casos extremos.
(Inexplicable: un bucle que involucra setcc ah
a veces puede ejecutarse desde el LSD, vea el rcr
bucle al final de esta publicación. Tal vez, siempre que ah
esté limpio al final del bucle, ¿puede usar el LSD?).
Si ah
está sucio, setcc ah
se 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 al
ralentizada por conflictos de recursos de los uops para ah
. También la cadena inc ah
/ setcc ah
dep).
Creo que lo que sucede aquí es que setcc r8
siempre se implementa como lectura-modificación-escritura. Intel probablemente decidió que no valía la pena tener un setcc
uop de solo escritura para optimizar el setcc ah
caso, 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 -m32
lo 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, 123
retiró). 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, comoadd
/inc
, nodiv r8
omul 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]
oxor eax,eax
) borra el estado sucio de AH (sin fusionar uop).La escritura de solo en AX (
mov ax, 1
omov 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,ah
tiene una latencia de 1c, no interrumpe el almacenamiento y aún necesita un puerto de ejecución.mov ah,0
es el mismo; al igual que consetcc
, 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, dl
puede 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
/ pop
evita 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
oradd 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
ortimes 2
mov ah, cl
: 2 cycles / iter.
(4 or 5 uops issued and executed / iter) - With
times 3
totimes 6
mov ah, cl
: 3 cycles / iter
(6 to 10 uops issued and executed) - With
times 7
totimes 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, cl
que 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,ah
or mov ah,0
crea una cadena de dependencia transmitida por bucle, exactamente como inc ah
, lo que confirma que mov ax, bx
tiene una dependencia de AH. Limita la velocidad a n
+3 ciclos por iteración, donde n
está la longitud de la times n mov ah,0
cadena. Por ejemplo, times 7
se 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, bx
costo 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, al
mantiene la dependencia de RAX, pero sin leer AH.
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, movsx
nunca se elimina y solo había 3 ALU antes de Haswell).
Pero aparentemente movsx r16, r8
la 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 movsx
instrucciones escritas en eax..edi y r10w/r11w, todas leyendo desde cl
. Y a dec ebp/jnz
como 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 dx
por 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.58c
rendimiento 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, r8
ignorar 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_8
prueba 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_8
midió una latencia de 1c en Sandybridge ! Entonces, tal vez mi teoría estaba equivocada sobre lo que movsx r16, r8
nos 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, r16
es 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?