Qual microarquitetura da Intel apresentou o caso especial ADC reg, 0 single-uop?

O ADC em Haswell e versões anteriores normalmente são 2 uops, com latência de 2 ciclos, porque os uops da Intel tradicionalmente só podiam ter 2 entradas (https://agner.org/optimize/) Broadwell / Skylake e, posteriormente, possuem ADC / SBB / CMOV de uop único, depois que Haswell introduziu uops de 3 entradas para FMA emicro-fusão de modos de endereçamento indexado em alguns casos.

(Mas não para oadc al, imm8 codificação de formato curto ou os outros al / ax / eax / rax, imm8 / 16/32/32 formatos curtos sem ModRM. Mais detalhes na minha resposta.)

Masadc com 0 imediato é especificada em Haswell para decodificar como apenas um único uop. @BeeOnRope testou issoe incluiu uma verificação para estepeculiaridade de desempenho em seu banco-uarch:https://github.com/travisdowns/uarch-bench. Saída de amostra deIC em um servidor Haswell mostrando uma diferença entreadc reg,0 eadc reg,1 ouadc reg,zeroed-reg.

(O mesmo para a SBB. Até onde eu vi, nunca há diferença entre o desempenho da ADC e da SBB para a codificação equivalente com o mesmo imediato em qualquer CPU.)

Quando foi essa otimização paraimm=0 introduzido?

$22https://agner.org/optimize/23$adc eax,0 latência é de 2 ciclos, o mesmo queadc eax,3. Além disso, a contagem de ciclos é idêntica para algumas variações dos testes de produtividade com0 vs.3, para que o Core 2 de primeira geração (Conroe / Merom) não faça essa otimização.

A maneira mais fácil de responder a isso é provavelmente usar meu programa de teste abaixo em um sistema Sandybridge e verificar seadc eax,0 é mais rápido queadc eax,1. Mas respostas baseadas em documentação confiável também seriam boas.

(BTW, se alguém tiver acesso a contadores de desempenho em um Sandybridge, você também poderá esclarecer o mistério deO desempenho é reduzido ao executar loops cuja contagem de UOPs não é um múltiplo da largura do processador? executando o código de teste do @ BeeOnRope. Ou foi a queda no desempenho que observei no meu SnB que não trabalha mais apenas como resultado de a laminação ser diferente dos uops normais?)

Nota de rodapé 1: Usei esse programa de teste no meu Core 2 E6600 (Conroe / Merom), executando o 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 não funciona muito bem em CPUs antigas como o Core 2 (não sabe como acessar todos os eventos como uops), mas sabe ler os contadores de HW para obter ciclos e instruções. Isso é suficiente.

Eu construí e perfilei isso com

 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 é o número interessante aqui.

É sobre o que esperaríamos da análise estática com uma latência de 2 uop / 2cadc: (5*(1+3) + 3) = 23 instruções no loop,5*(2+3) = 25 ciclos de latência = ciclos por iteração de loop. 23/25 = 0,92.

São 1,15 no Skylake.(5*(1+3) + 3) / (5*(1+3)) = 1.15, ou seja, o 0,15 extra é do xor-zero e dec / jg, enquanto a cadeia adc / add é executada exatamente a 1 uop por relógio, com gargalo na latência. Esperamos esse IPC geral de 1,15 em qualquer outro uarch com latência de ciclo únicoadctambém porque o front-end não é um gargalo. (Atom e P5 Pentium em ordem seriam um pouco mais baixos, mas xor e dec podem emparelhar com adc ou adicionar P5.)

No SKL,uops_issued.any = instructions = 2.303G, confirmando queadc é um uop único (que sempre está no SKL, independentemente do valor que o imediato tem). Por acaso,jg é a primeira instrução em uma nova linha de cache, para que não se funda comdec no SKL. Comdec rbp ousub ebp,1 em vez de,uops_issued.any é o 2.2G esperado.

Isso é extremamente repetitivo:perf stat -r5 (executá-lo 5 vezes e mostrar média + variância), e várias execuções disso mostraram que a contagem de ciclos era repetível em 1 parte em 1000. Latência 1c vs. 2c emadc faria umMuito de diferença maior que isso.

Reconstruindo o executável com um imediato diferente de0 não muda o tempoem absoluto no Core 2, outro forte sinal de que não há um caso especial. Definitivamente vale a pena testar.

Eu estava olhando inicialmente para o throughput (comxor eax,eax antes de cada iteração de loop, permitindo que o OoO exec se sobreponha às iterações), mas era difícil descartar efeitos de front-end. Acho que finalmentefez evite um gargalo de front-end adicionando o uop únicoadd instruções. A versão de teste de taxa de transferência do loop interno é assim:

    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

É por isso que a versão de teste de latência parece meio estranha. De qualquer forma, lembre-se de que o Core2 não possui um cache de uop decodificado e seu buffer de loop está no estágio de pré-decodificação (depois de encontrar os limites das instruções). Somente 1 dos 4 decodificadores pode decodificar instruções multi-uop, portantoadc sendo gargalos multi-uop no front-end. Eu acho que eu poderia ter deixado isso acontecer, comtimes 5 adc eax, 0, pois é improvável que algum estágio posterior do pipeline seja capaz de jogar fora esse uop sem executá-lo.

O buffer de loop de Nehalem recicla uops decodificados e evitaria esse gargalo de decodificação para obter instruções multi-uop consecutivas.

questionAnswers(2)

yourAnswerToTheQuestion