Todas as instruções a seguir fazem a mesma coisa: definir %eax
como zero. Qual caminho é o ideal (exigindo menos ciclos de máquina)?
xorl %eax, %eax
mov $0, %eax
andl $0, %eax
Todas as instruções a seguir fazem a mesma coisa: definir %eax
como zero. Qual caminho é o ideal (exigindo menos ciclos de máquina)?
xorl %eax, %eax
mov $0, %eax
andl $0, %eax
Respostas:
Resumo de TL; DR : xor same, same
é a melhor escolha para todas as CPUs . Nenhum outro método tem qualquer vantagem sobre ele e tem pelo menos alguma vantagem sobre qualquer outro método. É oficialmente recomendado pela Intel e AMD, e o que os compiladores fazem. No modo de 64 bits, ainda use xor r32, r32
, porque escrever um registro de 32 bits zera o 32 superior . xor r64, r64
é um desperdício de byte, porque precisa de um prefixo REX.
Pior ainda, o Silvermont reconhece apenas xor r32,r32
como quebra de dep, não o tamanho do operando de 64 bits. Portanto, mesmo quando um prefixo REX ainda é necessário porque você está zerando r8..r15, use xor r10d,r10d
, notxor r10,r10
.
Exemplos de inteiros GP:
xor eax, eax ; RAX = 0. Including AL=0 etc.
xor r10d, r10d ; R10 = 0
xor edx, edx ; RDX = 0
; small code-size alternative: cdq ; zero RDX if EAX is already zero
; SUB-OPTIMAL
xor rax,rax ; waste of a REX prefix, and extra slow on Silvermont
xor r10,r10 ; bad on Silvermont (not dep breaking), same as r10d everywhere else because a REX prefix is still needed for r10d or r10.
mov eax, 0 ; doesn't touch FLAGS, but not faster and takes more bytes
and eax, 0 ; false dependency. (Microbenchmark experiments might want this)
sub eax, eax ; same as xor on most but not all CPUs; bad on Silvermont for example.
xor al, al ; false dep on some CPUs, not a zeroing idiom. Use xor eax,eax
mov al, 0 ; only 2 bytes, and probably better than xor al,al *if* you need to leave the rest of EAX/RAX unmodified
Normalmente, é melhor zerar um registro vetorial com pxor xmm, xmm
. Isso é tipicamente o que o gcc faz (mesmo antes de usar as instruções FP).
xorps xmm, xmm
pode fazer sentido. É um byte a menos pxor
, mas xorps
precisa da porta 5 de execução no Intel Nehalem, enquanto pxor
pode ser executado em qualquer porta (0/1/5). (A latência de atraso de bypass 2c de Nehalem entre inteiro e FP geralmente não é relevante, porque a execução fora de ordem pode normalmente ocultá-la no início de uma nova cadeia de dependência).
Em microarquiteturas da família SnB, nenhum tipo de xor-zeroing precisa de uma porta de execução. No AMD e pré-Nehalem P6 / Core2 Intel, xorps
e pxor
são tratados da mesma maneira (como instruções de vetor-inteiro).
Usar a versão AVX de uma instrução de vetor 128b zera também a parte superior do reg, então vpxor xmm, xmm, xmm
é uma boa escolha para zerar YMM (AVX1 / AVX2) ou ZMM (AVX512), ou qualquer extensão de vetor futura. vpxor ymm, ymm, ymm
não leva bytes extras para codificar, porém, e roda da mesma forma na Intel, mas mais lento no AMD antes do Zen2 (2 uops). A zeragem do AVX512 ZMM exigiria bytes extras (para o prefixo EVEX), portanto, a zeragem XMM ou YMM deve ser preferida.
Exemplos XMM / YMM / ZMM
# Good:
xorps xmm0, xmm0 ; smallest code size (for non-AVX)
pxor xmm0, xmm0 ; costs an extra byte, runs on any port on Nehalem.
xorps xmm15, xmm15 ; Needs a REX prefix but that's unavoidable if you need to use high registers without AVX. Code-size is the only penalty.
# Good with AVX:
vpxor xmm0, xmm0, xmm0 ; zeros X/Y/ZMM0
vpxor xmm15, xmm0, xmm0 ; zeros X/Y/ZMM15, still only 2-byte VEX prefix
#sub-optimal AVX
vpxor xmm15, xmm15, xmm15 ; 3-byte VEX prefix because of high source reg
vpxor ymm0, ymm0, ymm0 ; decodes to 2 uops on AMD before Zen2
# Good with AVX512
vpxor xmm15, xmm0, xmm0 ; zero ZMM15 using an AVX1-encoded instruction (2-byte VEX prefix).
vpxord xmm30, xmm30, xmm30 ; EVEX is unavoidable when zeroing zmm16..31, but still prefer XMM or YMM for fewer uops on probable future AMD. May be worth using only high regs to avoid needing vzeroupper in short functions.
# Good with AVX512 *without* AVX512VL (e.g. KNL / Xeon Phi)
vpxord zmm30, zmm30, zmm30 ; Without AVX512VL you have to use a 512-bit instruction.
# sub-optimal with AVX512 (even without AVX512VL)
vpxord zmm0, zmm0, zmm0 ; EVEX prefix (4 bytes), and a 512-bit uop. Use AVX1 vpxor xmm0, xmm0, xmm0 even on KNL to save code size.
Consulte O vxorps-zeroing no AMD Jaguar / Bulldozer / Zen é mais rápido com registros xmm do que ymm? e
Qual é a maneira mais eficiente de limpar um ou alguns registros ZMM em Knights Landing?
Semi-relacionado: A maneira mais rápida de definir o valor __m256 para todos os bits ONE e
definir todos os bits no registro da CPU para 1 de forma eficiente também abrange os registros de k0..7
máscara AVX512 . SSE / AVX vpcmpeqd
é uma quebra de dep em muitos (embora ainda precise de um uop para escrever os 1s), mas AVX512 vpternlogd
para ZMM regs não é nem mesmo uma quebra de dep. Dentro de um loop, considere copiar de outro registrador em vez de recriar alguns com um uop ALU, especialmente com AVX512.
Mas zerar é barato: xor-zerar um reg xmm dentro de um loop geralmente é tão bom quanto copiar, exceto em algumas CPUs AMD (Bulldozer e Zen) que têm eliminação mov para regs vetoriais, mas ainda precisam de um uop ALU para escrever zeros para xor -zeroing.
Algumas CPUs reconhecem sub same,same
como um idioma de zeragem xor
, mas todas as CPUs que reconhecem qualquerxor
idioma de zeragem o reconhecem . Use apenas xor
para não precisar se preocupar com qual CPU reconhece qual idioma de zeragem.
xor
(sendo um idioma zeroing reconhecido, ao contrário mov reg, 0
) tem algumas vantagens óbvias e algumas vantagens sutis (lista de resumo, então irei expandir sobre elas):
mov reg,0
. (Todas as CPUs)O tamanho do código de máquina menor (2 bytes em vez de 5) é sempre uma vantagem: a densidade de código mais alta leva a menos erros do cache de instrução e melhor busca de instrução e potencialmente decodifica a largura de banda.
O benefício de não usar uma unidade de execução para xor nas microarquiteturas da família Intel SnB é menor, mas economiza energia. É mais provável que importe no SnB ou IvB, que tem apenas 3 portas de execução ALU. Haswell e posteriores têm 4 portas de execução que podem lidar com instruções ALU inteiras, incluindo mov r32, imm32
, portanto, com uma tomada de decisão perfeita pelo agendador (o que nem sempre acontece na prática), HSW ainda pode sustentar 4 uops por clock mesmo quando todos precisam de ALU portas de execução.
Veja minha resposta em outra pergunta sobre zerar registros para mais detalhes.
A postagem do blog de Bruce Dawson que Michael Petch vinculou (em um comentário sobre a questão) aponta que isso xor
é tratado no estágio de registro-renomeação sem a necessidade de uma unidade de execução (zero uops no domínio não fundido), mas deixou passar o fato de que ainda é um uop no domínio fundido. CPUs modernas da Intel podem emitir e retirar 4 uops de domínio fundido por clock. É daí que vem o limite de 4 zeros por clock. O aumento da complexidade do hardware de renomeação de registros é apenas uma das razões para limitar a largura do design a 4. (Bruce escreveu algumas postagens de blog muito excelentes, como sua série sobre matemática FP e questões de x87 / SSE / arredondamento , que eu faço altamente recomendado).
Em CPUs da família AMD Bulldozer , mov immediate
roda nas mesmas portas de execução de inteiros EX0 / EX1 que xor
. mov reg,reg
também pode ser executado em AGU0 / 1, mas isso é apenas para cópia de registro, não para configuração de imediatos. Então AFAIK, na AMD a única vantagem a xor
mais mov
é o mais curto de codificação. Também pode economizar recursos de registro físico, mas não vi nenhum teste.
Expressões idiomáticas de zeragem reconhecidas evitam penalidades de registro parcial em CPUs Intel que renomeiam registros parciais separadamente de registros completos (famílias P6 e SnB).
xor
irá marcar o registro como tendo as partes superiores zeradas , então xor eax, eax
/ inc al
/ inc eax
evita a penalidade usual de registro parcial que as CPUs pré-IvB têm. Mesmo sem xor
, o IvB só precisa de um uop de fusão quando os 8bits ( AH
) altos são modificados e então todo o registro é lido, e o Haswell até remove isso.
Do guia de microarca da Agner Fog, página 98 (seção do Pentium M, referenciada por seções posteriores, incluindo SnB):
O processador reconhece o XOR de um registrador consigo mesmo, definindo-o como zero. Uma tag especial no registro lembra que a parte alta do registro é zero, de modo que EAX = AL. Esta tag é lembrada mesmo em um loop:
; Example 7.9. Partial register problem avoided in loop xor eax, eax mov ecx, 100 LL: mov al, [esi] mov [edi], eax ; No extra uop inc esi add edi, 4 dec ecx jnz LL
(na página 82): O processador lembra que os 24 bits superiores do EAX são zero, desde que você não obtenha uma interrupção, previsão incorreta ou outro evento de serialização.
A pág82 desse guia também confirma que nãomov reg, 0
é reconhecido como um idioma de zeragem, pelo menos nos primeiros projetos P6 como PIII ou PM. Eu ficaria muito surpreso se eles gastassem transistores para detectá-lo em CPUs posteriores.
xor
define sinalizadores , o que significa que você deve ter cuidado ao testar as condições. Uma vez que, setcc
infelizmente, só está disponível com um destino de 8 bits , geralmente você precisa tomar cuidado para evitar penalidades de registro parcial.
Teria sido bom se o x86-64 redirecionasse um dos opcodes removidos (como AAM) para um bit 16/32/64 setcc r/m
, com o predicado codificado no campo de 3 bits do registrador de origem do campo r / m (o caminho algumas outras instruções de operando único os usam como bits de opcode). Mas eles não fizeram isso e, de qualquer maneira, isso não ajudaria no x86-32.
Idealmente, você deve usar xor
/ set flags / setcc
/ read full register:
...
call some_func
xor ecx,ecx ; zero *before* the test
test eax,eax
setnz cl ; cl = (some_func() != 0)
add ebx, ecx ; no partial-register penalty here
Isso tem um desempenho ideal em todas as CPUs (sem interrupções, uops mesclados ou dependências falsas).
As coisas são mais complicadas quando você não quer corrigir antes de uma instrução de definição de sinalizador . por exemplo, você deseja ramificar em uma condição e então setcc em outra condição dos mesmos sinalizadores. por exemplo cmp/jle
, sete
e você não quer ter um registo de reposição, ou você quer manter o xor
para fora do caminho de código não-tomadas por completo.
Não há expressões idiomáticas de zeramento reconhecidas que não afetem os sinalizadores, então a melhor escolha depende da microarquitetura de destino. No Core2, inserir um uop de fusão pode causar um bloqueio de 2 ou 3 ciclos. Parece ser mais barato no SnB, mas não gastei muito tempo tentando medir. Usar mov reg, 0
/ setcc
teria uma penalidade significativa em CPUs Intel mais antigas e ainda seria um pouco pior em processadores Intel mais novos.
Usar setcc
/ movzx r32, r8
é provavelmente a melhor alternativa para as famílias Intel P6 e SnB, se você não puder xou-zero antes da instrução de configuração de sinalizador. Isso deve ser melhor do que repetir o teste após um xor-zero. (Nem mesmo considere sahf
/ lahf
ou pushf
/ popf
). O IvB pode eliminar movzx r32, r8
(ou seja, tratá-lo com renomeação de registro sem unidade de execução ou latência, como xor-zeroing). Haswell e posteriores apenas eliminam mov
instruções regulares , portanto, movzx
leva uma unidade de execução e tem latência diferente de zero, tornando o teste / setcc
/ movzx
pior do que xor
/ teste / setcc
, mas ainda pelo menos tão bom quanto o teste / mov r,0
/ setcc
(e muito melhor em CPUs mais antigas).
Usar setcc
/ movzx
sem zerar primeiro é ruim no AMD / P4 / Silvermont, porque eles não rastreiam dependências separadamente para sub-registros. Haveria um falso dep no valor antigo do registro. Usar mov reg, 0
/ setcc
para zerar / quebrar a dependência é provavelmente a melhor alternativa quando xor
/ test / setcc
não é uma opção.
Obviamente, se você não precisa que setcc
a saída seja maior que 8 bits, não é necessário zerar nada. No entanto, cuidado com as falsas dependências em CPUs diferentes de P6 / SnB se você escolher um registrador que recentemente fez parte de uma longa cadeia de dependências. (E tome cuidado para não causar um registro parcial ou uop extra se você chamar uma função que pode salvar / restaurar o registro do qual você está usando parte.)
and
com um zero imediato não é especial como independente do valor antigo em quaisquer CPUs que eu conheça, portanto, não quebra as cadeias de dependência. Não tem vantagens xor
e muitas desvantagens.
É útil apenas para escrever microbenchmarks quando você deseja uma dependência como parte de um teste de latência, mas deseja criar um valor conhecido zerando e adicionando.
Consulte http://agner.org/optimize/ para obter detalhes de microarch , incluindo quais expressões idiomáticas de zeragem são reconhecidas como quebra de dependência (por exemplo, sub same,same
é em algumas, mas não todas as CPUs, enquanto xor same,same
é reconhecido em todas.) mov
Quebra a cadeia de dependência do valor antigo do registro (independente do valor da fonte, zero ou não, pois é assim que mov
funciona). xor
somente quebra as cadeias de dependências no caso especial onde src e dest são o mesmo registrador, que é o motivo pelo qual mov
é deixado de fora da lista de separadores de dependências especialmente reconhecidos. (Além disso, porque não é reconhecido como um idioma de zeragem, com os outros benefícios que traz.)
Curiosamente, o projeto P6 mais antigo (PPro até Pentium III) não reconhecia a xor
-zeroing como um eliminador de dependência, apenas como um idioma de zeragem com o objetivo de evitar paralisações de registro parcial , então em alguns casos valeu a pena usar ambos mov
e então xor
- zerar nessa ordem para quebrar o dep e então zero novamente + definir o bit interno da tag de que os bits altos são zero, então EAX = AX = AL.
Veja o Exemplo 6.17 de Agner Fog. em seu pdf microarch. Ele diz que isso também se aplica a P2, P3 e até (cedo?) PM. Um comentário no post do blog vinculado diz que foi apenas o PPro que teve esse descuido, mas eu testei no Katmai PIII e @Fanael testei em um Pentium M, e ambos descobrimos que ele não quebrou a dependência de uma latência imul
cadeia de ligação . Isso confirma os resultados de Agner Fog, infelizmente.
Se isso realmente torna seu código mais agradável ou salva instruções, então com certeza, zere com mov
para evitar tocar nos sinalizadores, contanto que você não introduza um problema de desempenho diferente do tamanho do código. Evitar a destruição dos sinalizadores é a única razão sensata para não usar xor
, mas às vezes você pode xou-zero antes do que define os sinalizadores se você tiver um registrador sobressalente.
mov
-zero à frente setcc
é melhor para latência do que movzx reg32, reg8
depois (exceto na Intel quando você pode escolher registros diferentes), mas pior tamanho de código.
mov reg, src
também quebra as cadeias de dep para CPUs OO (independentemente de src ser imm32 [mem]
ou outro registrador). Essa quebra de dependência não é mencionada em manuais de otimização porque não é um caso especial que só acontece quando src e dest são o mesmo registrador. Isso sempre acontece para instruções que não dependem de seu destino. (exceto para a implementação da Intel de popcnt/lzcnt/tzcnt
ter uma dependência falsa no destino)
mov
liberta, apenas latência zero. A parte "não pegar uma porta de execução" geralmente não é importante. A taxa de transferência de domínio fundido pode facilmente ser o gargalo, esp. com cargas ou lojas no mix.
xor r64, r64
, não desperdiça apenas um byte. Como você diz xor r32, r32
é a melhor escolha especialmente com KNL. Consulte a seção 15.7 "Casos especiais de independência" neste manual microarquista se quiser ler mais.