Qué microarquitectura de Intel presentó el caso especial ADC reg, 0 single-uop?
ADC en Haswell y versiones anteriores son normalmente 2 uops, con latencia de 2 ciclos, porque Intel uops tradicionalmente solo podía tener 2 entradas https: //agner.org/optimize). Broadwell / Skylake y más tarde tienen ADC / SBB / CMOV de un solo uop, después de que Haswell introdujo uops de 3 entradas para FMA y micro fusión de modos de direccionamiento indexado en algunos casos
(Pero no para eladc al, imm8
codificación de forma corta, u otras formas cortas al / ax / eax / rax, imm8 / 16/32/32 sin ModRM. Más detalles en mi respuesta.)
Peroadc
con un 0 inmediato está revestido de forma especial en Haswell para decodificar como una sola uop. @ BeeOnRope probó esto e incluyó un cheque para esta rendimiento peculiar en su banco de uarch:https: //github.com/travisdowns/uarch-benc. Salida de muestra deCI en un servidor Haswell mostrando una diferencia entreadc reg,0
yadc reg,1
oadc reg,zeroed-reg
.
(Lo mismo para SBB. Por lo que he visto, nunca hay diferencia entre el rendimiento de ADC y SBB para la codificación equivalente con la misma inmediata en cualquier CPU).
Cuando fue esta optimización paraimm=0
¿introducido
He probado en Core 21, y encontré queadc eax,0
latencia es de 2 ciclos, igual queadc eax,3
. Y también el recuento de ciclos es idéntico para algunas variaciones de pruebas de rendimiento con0
vs.3
, por lo que el Core 2 de primera generación (Conroe / Merom) no hace esta optimización.
La forma más fácil de responder esto es probablemente usar mi programa de prueba a continuación en un sistema Sandybridge, y ver siadc eax,0
es más rápido queadc eax,1
. Pero las respuestas basadas en documentación confiable también estarían bien.
(Por cierto, si alguien tiene acceso a los contadores de rendimiento en un Sandybridge, también podría aclarar el misterio deSe reduce el rendimiento al ejecutar bucles cuyo recuento de UOP no es un múltiplo del ancho del procesador? ejecutando el código de prueba de @ BeeOnRope. ¿O el rendimiento en picado que observé en mi SnB que ya no funcionaba es solo el resultado de que la laminación sea diferente de los Uops normales?)
Footnote 1: Utilicé este programa de prueba en mi Core 2 E6600 (Conroe / Merom), ejecutando Linux.
;; NASM / YASM
;; assemble / link this into a 32 or 64-bit static executable.
global _start
_start:
mov ebp, 100000000
align 32
.loop:
xor ebx,ebx ; avoid partial-flag stall but don't break the eax dependency
%rep 5
adc eax, 0 ; should decode in a 2+1+1+1 pattern
add eax, 0
add eax, 0
add eax, 0
%endrep
dec ebp ; I could have just used SUB here to avoid a partial-flag stall
jg .loop
%ifidn __OUTPUT_FORMAT__, elf32
;; 32-bit sys_exit would work in 64-bit executables on most systems, but not all. Some, notably Window's subsystem for Linux, disable IA32 compat
mov eax,1
xor ebx,ebx
int 0x80 ; sys_exit(0) 32-bit ABI
%else
xor edi,edi
mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h
syscall ; sys_exit_group(0)
%endif
Linuxperf
no funciona muy bien en CPU antiguas como Core 2 (no sabe cómo acceder a todos los eventos como uops), pero sí sabe cómo leer los contadores HW para ciclos e instrucciones. Eso es suficiente.
Construí y perfilé esto con
yasm -felf64 -gdwarf2 testloop.asm
ld -o testloop-adc+3xadd-eax,imm=0 testloop.o
# optional: taskset pins it to core 1 to avoid CPU migrations
taskset -c 1 perf stat -e task-clock,context-switches,cycles,instructions ./testloop-adc+3xadd-eax,imm=0
Performance counter stats for './testloop-adc+3xadd-eax,imm=0':
1061.697759 task-clock (msec) # 0.992 CPUs utilized
100 context-switches # 0.094 K/sec
2,545,252,377 cycles # 2.397 GHz
2,301,845,298 instructions # 0.90 insns per cycle
1.069743469 seconds time elapsed
0.9 IPC es el número interesante aquí.
Esto es lo que esperaríamos del análisis estático con una latencia de 2 uop / 2cadc
: (5*(1+3) + 3) = 23
instrucciones en el bucle,5*(2+3) = 25
ciclos de latencia = ciclos por iteración de bucle. 23/25 = 0,92.
Son las 1.15 en Skylake. @(5*(1+3) + 3) / (5*(1+3)) = 1.15
, es decir, el .15 extra proviene de xor-zero y dec / jg, mientras que la cadena adc / add funciona exactamente a 1 uop por reloj, con cuello de botella en la latencia. Esperaríamos este IPC general de 1.15 en cualquier otro uarch con latencia de ciclo únicoadc
, también, porque el front-end no es un cuello de botella. (En orden Atom y P5 Pentium serían ligeramente más bajos, pero xor y dec pueden emparejarse con adc o agregar P5.)
En SKL,uops_issued.any
= instructions
= 2.303G, confirmando queadc
es single uop (que siempre está en SKL, independientemente del valor que tenga el inmediato). Por casualidad,jg
es la primera instrucción en una nueva línea de caché, por lo que no se fusiona condec
en SKL. Condec rbp
osub ebp,1
en cambio,uops_issued.any
es el esperado 2.2G.
Esto es extremadamente repetible:perf stat -r5
(para ejecutarlo 5 veces y mostrar promedio + varianza), y varias ejecuciones de eso, mostraron que el conteo del ciclo era repetible a 1 parte en 1000. Latencia 1c vs. 2c enadc
haría unmuch mayor diferencia que eso.
Reconstrucción del ejecutable con un inmediato distinto de0
no cambia el tiempoen absolut en Core 2, otra señal fuerte de que no hay un caso especial. Definitivamente vale la pena probarlo.
nicialmente estaba mirando el rendimiento (conxor eax,eax
antes de cada iteración de bucle, permitiendo que el ejecutivo de OoO se superponga a las iteraciones), pero fue difícil descartar efectos de front-end. Creo que finalmentehiz evite un cuello de botella en la interfaz agregando single-uopadd
instrucciones. La versión de prueba de rendimiento del bucle interno se ve así:
xor eax,eax ; break the eax and CF dependency
%rep 5
adc eax, 0 ; should decode in a 2+1+1+1 pattern
add ebx, 0
add ecx, 0
add edx, 0
%endrep
Es por eso que la versión de prueba de latencia se ve un poco rara. Pero de todos modos, recuerde que Core2 no tiene una caché decod-uop, y su búfer de bucle está en la etapa de predescodificación (después de encontrar los límites de las instrucciones). Solo 1 de los 4 decodificadores puede decodificar instrucciones multi-uop, entoncesadc
siendo cuellos de botella de múltiples uop en el front-end. Supongo que podría haber dejado que eso sucediera, contimes 5 adc eax, 0
, ya que es poco probable que alguna etapa posterior de la tubería pueda deshacerse de esa UOP sin ejecutarla.
El búfer de bucle de Nehalem recicla uops decodificados y evitaría ese cuello de botella de decodificación para obtener instrucciones de múltiples uop consecutivas.