assembly - ¿Cómo funcionan exactamente los registros parciales en Haswell/Skylake? Escribir AL parece tener una dependencia falsa en RAX, y AH es inconsistente
x86 intel (2)
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 se renombra por separado de RAX (o r15b de r15). Entonces, si nunca toca los registros high8 (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 una dependencia de RAX. Para cargas en AL, esta es una carga de ALU + micro fusionada 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 uop para AL, por lo que probablemente se renombra por separado de RAX. Para SnB, el manual de optimización de Intel (sección 3.5.2.4 Puestos de registro parcial) dice
SnB (no necesariamente uarches posteriores) inserta una fusión uop en los siguientes casos:
Después de escribir en 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 estos casos, se inserta una fusión micro-op. 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 la siguiente lectura de una forma de 2, 4 u 8 bytes del mismo registro En estos casos, la fusión micro-op es parte del flujo .
Creo que dicen que en SnB,
add al,bl
RMW RMX completo en lugar de renombrarlo 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 fusionando uops todavía tiene que emitir / cambiar el nombre por su cuenta en HSW / SKL. Eso haría que el impacto frontal sea equivalente a 4 uops (ya que ese es el problema / cambiar el nombre del ancho de la tubería).
-
No hay forma de romper una dependencia que involucra a AL sin escribir EAX / RAX.
xor al,al
no ayuda, y tampoco lo hacemov al, 0
. -
movzx ebx, al
tiene latencia cero (renombrado) y no necesita unidad de ejecución. (es decir, mov-elimination funciona en HSW y SKL). Activa la fusión de AH si está sucio , lo que supongo que es necesario para que funcione sin una ALU. Probablemente no sea una coincidencia que Intel haya reducido el cambio de nombre en el mismo lugar 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-elimination en Intel nunca funciona para lo mismo, lo mismo.mov rax,rax
tampoco se elimina, aunque no tiene que extender nada a cero. (Aunque no tendría sentido darle soporte especial de hardware, porque es solo un no-op, a diferencia demov eax,eax
). De todos modos, prefiera moverse entre dos registros arquitectónicos separados cuando se extienda a cero, ya sea con unmov
32 bits o unmovzx
8 bits. -
movzx eax, bx
no se elimina al cambiar el nombre en HSW o SKL. Tiene una latencia de 1c y usa una ALU uop. El manual de optimización de Intel solo menciona la latencia cero para movzx de 8 bits (y señala quemovzx r32, high8
nunca cambia de nombre).
Los registros High-8 se pueden renombrar por separado del resto del registro, y es necesario fusionar uops.
-
Acceso de solo escritura a
ah
conmov ah, r8
omov ah, [mem]
cambia el nombre de AH, sin dependencia del valor anterior. Ambas 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 viejoah
, pero aún lo ensucia. Creo quemov ah, imm8
es lo mismo, pero no he probado tantos casos de esquina.(Inexplicable: un bucle que involucra
setcc ah
veces puede ejecutarse desde el LSD, vea el buclercr
al final de esta publicación. Tal vez mientrasah
esté limpio al final del bucle, ¿puede usar el LSD?).Si
ah
está sucio,setcc ah
fusiona en elah
renombrado, en lugar de forzar una fusión enrax
. Por ejemplo,%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 (la latencia de 8inc al
ralentiza por conflictos de recursos de los uops paraah
También la cadenainc ah
/setcc ah
dep).Creo que lo que está sucediendo aquí es que
setcc r8
siempre se implementa como lectura-modificación-escritura. Intel probablemente decidió que no valía la pena tener unsetcc
uop de solosetcc
para optimizar el caso desetcc ah
, ya que es muy raro que el código generado por el compiladorsetcc ah
. (Pero vea el enlace godbolt en la pregunta: clang4.0 con-m32
lo hará). -
la lectura de AX, EAX o RAX desencadena una fusión uop (que ocupa el ancho de banda del problema de front-end / cambio de nombre). Probablemente, la RAT (tabla de asignación de registros) rastrea el estado alto de 8 sucios 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 ROB = 224 en SKL, por lo que esto garantiza que el
mov ah, 123
fue retirado). Detectado con uops_issued / ejecutados contadores de rendimiento, que muestran claramente la diferencia. -
La lectura-modificación-escritura de AL (por ejemplo,
inc al
) se combina de forma gratuita, como parte de la UOP ALU. (Solo probado con unos pocos uops simples, comoadd
/inc
, nodiv r8
omul r8
). Una vez más, no se activa la fusión uop incluso si AH está sucio. -
Escribir solo en EAX / RAX (como
lea eax, [rsi + rcx]
oxor eax,eax
) borra el estado AH-sucio (sin fusión uop). -
Escribir solo en AX (
mov ax, 1
) activa primero una fusión de AH. Supongo que en lugar de una carcasa especial, funciona como cualquier otro RMW de AX / RAX. (TODO: pruebamov ax, bx
, aunque eso no debería ser especial porque no ha cambiado de nombre). -
xor ah,ah
tiene una latencia de 1c, no es de última generación y aún necesita un puerto de ejecución. -
La lectura y / o escritura de AL no fuerza una fusión, por lo que AH puede permanecer sucio (y ser usado independientemente en una cadena de dep separada).
(por ejemplo,
add ah, cl
/add al, dl
puede ejecutarse a 1 por reloj (con cuellos de botella en la latencia de agregar).
Hacer que AH se ensucie evita 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 IDQ).
Insertar Uops de fusión 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
/
pop
no coincidentes, lo que tiene sentido, pero implica que
puede
ejecutar bucles con
push
/
pop
equilibrado.
Eso no es lo que estoy viendo en SKL: incluso el
push
/
pop
equilibrado 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 el HSW / SKL:
SnB puede simplemente "bloquear" los uops en el IDQ en lugar de repetirlos varias veces, por lo que un ciclo 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 de motor de pila.
Este comportamiento puede estar relacionado con una errata en SKL :
Problema: en condiciones complejas de microarquitectura, 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 declaración del manual de optimización de Intel de que SnB al menos tiene que emitir / cambiar el nombre de una fusión de AH en un ciclo por sí mismo. 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
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 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 pueden ejecutar el
setne ah
/
add ah,ah
/
rcr ebx,1
/
mov eax,ebx
loop desde el LSD).
Tengo HT habilitado (que es una condición previa para que se manifieste SKL150), pero estaba probando en un sistema inactivo en su mayoría, por lo que mi hilo tenía el núcleo en sí mismo.
Con el microcódigo actualizado, el LSD está completamente deshabilitado para todo todo el tiempo, no solo cuando los registros parciales están activos.
lsd.uops
siempre es exactamente cero, incluso para programas reales, no bucles sintéticos.
Los errores de hardware (en lugar de los errores de microcódigo) a menudo requieren deshabilitar una función completa para solucionarlo.
Esta es la razón por la cual 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 la caché uop-cache de SKL sobre Broadwell casi siempre puede mantenerse 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 adicional de latencia para ambos operandos.
por ejemplo,
add bl, ah
tiene una latencia de 2c desde la entrada BL hasta 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 antes, con latencia vectorial en Skylake, donde un retraso int / float "contamina" un registro para siempre. TODO: escriba 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 está sucio no agrega ninguna latencia. (
add ah,ah
oadd ah,dh
/add dh,ah
tienen 1c de latencia 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 puede tomar bits [7: 0] de un registro físico como una lectura normal de registro de 8 bits.
La latencia adicional no significa un rendimiento reducido.
Este programa puede ejecutarse a 1 iter por 2 relojes, a pesar de que todas las instrucciones de
add
tienen una latencia de 2c (desde 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 una latencia transportada en bucle 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: se ejecuta desde el LSD, aunque ensucia AH.
(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 total) en ambos HSW / SKL.
(La tienda / recarga comentada también funciona, pero SKL tiene un reenvío de tiendas más rápido que HSW, y es de
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% )
Tenga en cuenta que ya no se ejecuta desde el LSD.
Este ciclo se ejecuta en una iteración por 3 ciclos en Intel Conroe / Merom, con un cuello de botella en el rendimiento total como se esperaba.
Pero en Haswell / Skylake, se ejecuta en una iteración por 11 ciclos, aparentemente porque
setnz al
depende de la última
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 en bucle.
Si no, cada cadena
imul
setcc
/
movzx
/ 3x es independiente, se bifurca a partir del
dec
que actualiza el contador de bucle.
El 11c por iteración medido 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).
Fuera del tema: evitar estos cuellos de botella (intencionales)
Estaba buscando un comportamiento comprensible / predecible para aislar cosas 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
).
Eso rompe el dep en eax en todas las CPU (excepto las primeras familias de P6 como PII y PIII), aún evita las penalizaciones por fusión de registros parciales y ahorra 1c de latencia
movzx
.
También utiliza una UOP de ALU menos en las CPU que
manejan la reducción a cero 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 en el 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
donde podría haber usado
xor
lugar de
movzx
(ejemplo del compilador-explorador Godbolt)
, 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 xor / dec / setnz se ejecuta a 3.0c por iteración en Skylake, Haswell y Core2 (con cuellos de botella en el rendimiento total).
xor
zeroing rompe la dependencia del valor anterior de
eax
en todas las CPU fuera de servicio que no sean PPro / PII / PIII / early-Pentium-M (donde aún evita penalizaciones por fusión de registro parcial pero no rompe el dep )
La guía de microarquitectura de Agner Fog describe esto
.
Reemplazando la reducción a cero con
mov eax,0
ralentiza a uno por 4.78 ciclos en Core2:
pérdida de 2-3c (¿en el extremo frontal?) Para insertar una unión parcial de registro parcial
cuando
Imul
lee
eax
después de la
setnz al
.
Además, utilicé
movzx eax, al
que derrota la eliminación de mov, tal como lo hace
mov rax,rax
.
(IvB, HSW y SKL pueden cambiar el nombre de
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 microarquitectura de Agner Fog , pero el comportamiento de HSW / SKL no lo es. De la sección 11.10 para Skylake, y lo mismo para uarches 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 grupo para volver a probar los supuestos, por lo que este cambio de comportamiento se escapó por las grietas.
Agner describe la inserción de una uop de fusión (sin bloqueo) 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 registros de high8. Avíseme si ve mis comentarios incorrectos en otras publicaciones, por lo que puedo eliminarlos o agregar una corrección. Intentaré al menos encontrar y editar mis respuestas donde he dicho esto).
Mis preguntas reales: ¿Cómo se comportan realmente los registros parciales en Skylake?
¿Es todo igual desde IvyBridge hasta Skylake, incluida 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 de high8) agregando latencia adicional incluso cuando no tienen No ha sido modificado.
Si hay algún comportamiento de la familia P6 (Core2 / Nehalem) que la guía de microarquitectura de Agner Fog no describe, eso también sería interesante, pero probablemente debería limitar el alcance de esta pregunta a solo Skylake o Sandybridge-family.
Mis datos de prueba de Skylake
, desde poner
%rep 4
secuencias cortas dentro de un pequeño
dec ebp/jnz
que ejecuta iteraciones de 100M o 1G.
Medí ciclos con Linux
perf
la misma manera que
en mi respuesta aquí
, en el mismo hardware (computadora de 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 adicionales de fusión.
Los casos "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 algo mejor que 1 por ciclo es una indicación de que el cambio de nombre del 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 para
lsd.uops
son exactamente 0 en HSW, y pequeños en SKL (alrededor de 1.8k) y no se escalan con el recuento de iteraciones de bucle.
Probablemente esos recuentos sean de algún código del núcleo.
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 (p. Ej., Cuando podrían no encajar en la memoria caché uop si la decodificación comienza en el lugar incorrecto), pero no me encontré con eso mientras lo probaba.
-
mov ah, bh
y / omov ah, bl
repetidosmov ah, bl
corre a 4 por ciclo. Se necesita una ALU uop, por lo que no se elimina comomov eax, ebx
. -
mov ah, [rsi]
repetidomov ah, [rsi]
ejecuta a 2 por ciclo (cuello de botella de rendimiento de carga). -
mov ah, 123
repetidomov ah, 123
carreras a 1 por ciclo. (Unxor eax,eax
dep-xor eax,eax
dentro del bucle elimina el cuello de botella). -
setz ah
osetc ah
ejecutan a 1 por ciclo. (Unxor eax,eax
últimaxor eax,eax
permite embotellar el rendimiento desetcc
parasetcc
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 del valor anterior, mientras quemov r8, r/m8
no (para reg o src de memoria)? (¿Y qué hay demov r/m8, r8
? Seguramente no importa cuál de los dosmov r/m8, r8
que utilices para los movimientos reg-reg?) -
repetido
add ah, 123
corre a 1 por ciclo, como se esperaba. -
repetido
add dh, cl
corre a 1 por ciclo. -
repetido
add dh, dh
corre 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).
Terminología
: Todos estos dejan AH (o DH) "
sucio
", es decir, necesitan fusionarse (con una fusión uop) 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 lo contrario.
Hay muchas formas de limpiar un registro sucio, la más simple es
inc eax
o
mov eax, esi
.
Escribir solo en AL
: estos bucles se ejecutan desde el LSD:
uops_issue.any
~ =
lsd.uops
.
-
mov al, bl
repetidomov al, bl
corre a 1 por ciclo. Unxor eax,eax
ocasional que rompe el depxor eax,eax
por grupo permite que el cuello de botella de ejecución de OOO afecte el rendimiento de UOP, no la latencia. -
mov al, [rsi]
repetidamentemov al, [rsi]
ejecuta a 1 por ciclo, como un ALU + micro-fusionado uop de carga. (uops_issued = 4G + bucle de sobrecarga, uops_executed = 8G + bucle de sobrecarga). Unxor eax,eax
últimaxor eax,eax
antes de un grupo de 4 le permite embotellar 2 cargas por reloj. -
mov al, 123
repetidomov al, 123
carreras a 1 por ciclo. -
mov al, bh
repetidomov al, bh
corre 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. -
repetido
add dl, ch
funciona a 0.5 por ciclo. (1 por 2 ciclos). Leer [ABCD] H aparentemente crea latencia extra paradl
. -
repetido
add dl, cl
corre a 1 por ciclo.
Creo que una escritura en un registro de bajo 8 se comporta como una mezcla de RMW en el registro completo, como
add eax, 123
sería, pero no desencadena una fusión si
ah
está sucio.
Entonces (aparte de ignorar la fusión
AH
) se comporta igual que en las CPU que no realizan ningún cambio de nombre de registro parcial.
Parece que
AL
nunca se renombra por separado de
RAX
?
-
inc al
paresinc al
/inc ah
pueden ejecutarse en paralelo. -
mov ecx, eax
inserta una fusión uop siah
está "sucio", pero se cambia el nombre delmov
real. Esto es lo que describe Agner Fog para IvyBridge y versiones posteriores. -
repetido
movzx eax, ah
ejecuta a uno por 2 ciclos. (La lectura de registros de 8 altos 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. (Al igual que 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. ( mov-elimination nunca funciona para elsame,same
caso , solo entre diferentes registros arquitectónicos).¿Un bucle que inserta una fusión uop cada iteración no puede ejecutarse desde el LSD (buffer de bucle)?
No creo que haya nada especial en AL / AH / RAX vs. B *, C *, DL / DH / RDX.
He probado algunos con registros parciales en otros registros (aunque en su mayoría estoy mostrando
AL
/
AH
por consistencia), y nunca he notado ninguna diferencia.
¿Cómo podemos explicar todas estas observaciones con un modelo sensible de cómo funciona el microarch interno?
Relacionado: Los problemas de
marca
parcial son diferentes de
los
problemas de
registro
parcial.
Vea la
instrucción INC vs ADD 1: ¿Importa?
para algunas cosas súper extrañas con
shr r32,cl
(e incluso
shr r32,2
en Core2 / Nehalem: no lea las banderas de un turno que no sea por 1).
Consulte también
Problemas con ADC / SBB e INC / DEC en bucles cerrados en algunas CPU
para cosas de bandera parcial en bucles
adc
.
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.
InstLatX64
resultados de InstLatX64 de SnB e IvB muestran un rendimiento de
movsx r16, r8
para
movsx r16, r8
(como se esperaba,
movsx
nunca se elimina y solo había 3 ALU antes de Haswell).
Pero al parecer, el
movsx r16, r8
InstLat
movsx r16, r8
prueba los cuellos de botella de Haswell / Broadwell / Skylake con un rendimiento de 1c (vea también
este informe de error en el github instlat
)
Probablemente escribiendo el mismo registro arquitectónico, creando una cadena de fusiones.
(El rendimiento real de esa instrucción con registros de destino separados es 0.25c en mi Skylake. Probado con 7 instrucciones
movsx
que escriben en eax..edi y r10w / r11w, todas leen desde
cl
. Y un
dec ebp/jnz
como la rama de bucle a haz un bucle de 8 uop.)
Si estoy acertando sobre lo que creó ese resultado de rendimiento 1c en las CPU
después de
IvB, está haciendo algo como ejecutar un bloque de
movsx dx, al
.
Y eso solo puede ejecutarse a más de 1 IPC en CPU que
dx
nombre de
dx
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 descartaron.
(
Pero aquí hay algo sospechoso: si esta explicación es correcta, deberíamos ver el mismo rendimiento de 1c en AMD que no cambia el nombre de los registros parciales. Pero no lo hacemos, ver más abajo
).
Resultados con un rendimiento de ~ 0.33c para las
movsx r16, r8
(y
movzx r16, r8
):
- IvB con AIDA64 build: 4.0.568.0 24 de mayo de 2013
- IvB-E build: 4.3.764.0 10 de julio de 2017
- SnB-EP con una versión 2013
- SnB con una versión 2018 .
Haswell resulta con un misterioso rendimiento de
movsx/zx r16, r8
para
movsx/zx r16, r8
:
- Un resultado de Haswell con la misma versión 4.3.764.0 del 10 de julio de 2017 de AIDA64
- Haswell-E con una construcción 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 5 de junio de 2013, BDW con 4.3.15787.0 12 de octubre de 2018, BDW con 4.3.739.0 17 de marzo de 2017.
Como informé en el problema vinculado InstLat en github, los números de "latencia" para
movzx r32, r8
ignoran la eliminación de mov, presumiblemente probando como
movzx eax, al
.
Peor aún, las versiones más nuevas de InstLatX64 con versiones de registros separados de la prueba, 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 una tontería total;
Lo probé solo para estar seguro.
La
MOVSX r1_16, r2_8
muestra la latencia 1c, por lo que aparentemente solo
miden la latencia de la dependencia (falsa) de salida
.
(Que no existe para salidas de 32 bits y más anchas).
¡Pero esa
MOVSX r1_16, r2_8
midió la latencia 1c
en Sandybridge
!
Entonces, tal vez mi teoría estaba equivocada sobre lo que nos dice la prueba
movsx r16, r8
.
En Ryzen (AIDA64 compilación 4.3.781.0 21 de febrero de 2018), que sabemos que no hace ningún cambio de nombre de registro parcial , los resultados no muestran el efecto de rendimiento 1c que esperaríamos si la prueba realmente estuviera escribiendo el mismo registro de 16 bits repetidamente. Tampoco lo encuentro en ninguna CPU AMD anterior, con versiones anteriores de InstLatX64, como K10 o la familia Bulldozer.
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 0.58c Haswell.
Los números MOVZX son los mismos, con un rendimiento de 0.25 para la versión sin prefijos que lee R8 y escribe un R32.
¿Tal vez hay un cuello de botella en buscar / decodificar para obtener instrucciones más grandes?
Pero
movsx r32, r16
es del mismo tamaño que
movsx r32, r8
.
Sin embargo, las pruebas de registro separado muestran el mismo patrón que en Intel, con latencia 1c solo para la que tiene que fusionarse. MOVZX es lo mismo.
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 esto, pero, por supuesto, un rendimiento menor.