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.

Respuestas a la pregunta(2)

Su respuesta a la pregunta