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 únicoadc
també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.