Qual é a melhor maneira de definir um registro para zero no assembly x86: xor, mov ou and?

Todas as instruções a seguir fazem a mesma coisa: set %eax para zero. Qual é o caminho ideal (requerendo menos ciclos de máquina)?

 xorl %eax, %eax mov $0, %eax andl $0, %eax 

TL; DR resumo : xor same, same é a melhor escolha para todos os processadores . Nenhum outro método tem alguma vantagem sobre ele e tem pelo menos alguma vantagem sobre qualquer outro método. É oficialmente recomendado pela Intel e pela AMD. No modo de 64 bits, ainda use xor r32, r32 , porque escrever um reg de 32 bits na parte superior 32 . xor r64, r64 é um desperdício de um byte, porque precisa de um prefixo REX.

Zerar um registrador vetorial geralmente é melhor feito com pxor xmm, xmm . Isso é tipicamente o que o gcc faz (mesmo antes de usar com instruções FP).

xorps xmm, xmm pode fazer sentido. É um byte mais curto que o pxor , mas o xorps precisa da porta de execução 5 do Intel Nehalem, enquanto o pxor pode ser executado em qualquer porta (0/1/5). (A latência de atraso de bypass de 2c do Nehalem entre o inteiro e o FP geralmente não é relevante, porque a execução fora de ordem normalmente pode ocultá-lo no início de uma nova cadeia de dependência).

Em microarquiteturas da família SnB, nem o sabor do xor-zero precisa de uma porta de execução. Na AMD, e pré-Nehalem P6 / Core2 Intel, xorps e pxor são tratados da mesma maneira (como instruções de inteiros vetoriais).

Usar a versão AVX de uma instrução vetorial de 128b zera também a parte superior do registro, portanto, vpxor xmm, xmm, xmm é uma boa opção para zerar YMM (AVX1 / AVX2) ou ZMM (AVX512) ou qualquer extensão vetorial futura. vpxor ymm, ymm, ymm não toma nenhum byte extra para codificar, e executa o mesmo. A zeragem do ZXM do AVX512 exigiria bytes extras (para o prefixo EVEX), portanto, o zeramento do XMM ou do YMM deve ser preferido.


Algumas CPUs reconhecem sub same,same uma linguagem de xor como xor , mas todas as CPUs que reconhecem quaisquer idiomas xor reconhecem xor . Apenas use xor para que você não precise se preocupar com qual CPU reconhece qual idioma de zeramento.

xor (sendo um idioma de zeramento reconhecido, ao contrário de mov reg, 0 ) tem algumas vantagens óbvias e algumas sutis (lista de resumo, então eu expandi-las):

  • tamanho de código menor que mov reg,0 . (Todas as CPUs)
  • evita penalidades de registro parcial para código posterior. (Família Intel P6 e família SnB).
  • não usa uma unidade de execução, economizando energia e liberando resources de execução. (Família Intel SnB)
  • uop menor (sem dados imediatos) deixa espaço na linha de cache do uop para instruções próximas a serem emprestadas, se necessário. (Família Intel SnB).
  • não usa inputs no arquivo de registro físico . (Intel família SnB (e P4), pelo menos, possivelmente AMD, uma vez que eles usam um design de PRF semelhante em vez de manter o estado de registro no ROB como microarquiteturas da família Intel P6.)

Tamanho de código de máquina menor (2 bytes em vez de 5) é sempre uma vantagem: Maior densidade de código leva a menos falhas de 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 em microarquiteturas da família Intel SnB é menor, mas economiza energia. É mais provável que importe em SnB ou IvB, que têm apenas 3 portas de execução da ALU. Haswell e mais tarde tem 4 portas de execução que podem manipular instruções ALU inteiras, incluindo mov r32, imm32 , então com a perfeita tomada de decisões pelo agendador (o que não acontece na prática), o HSW ainda pode suportar 4 uops por clock mesmo quando todos precisam de portas de execução.

Veja minha resposta em outra pergunta sobre zerar registros para mais alguns detalhes.

A postagem no blog de Bruce Dawson que Michael Petch vinculou (em um comentário sobre a questão) aponta que xor é manipulado no estágio de renomeação de registradores sem precisar de uma unidade de execução (zero uops no domínio não fundido), mas perdeu o fato de ser ainda um uop no domínio fundido. Os modernos processadores Intel podem emitir e remover 4 uops de domínio fundido por relógio. É aí que vem os 4 zeros por limite de clock. A complexidade crescente do hardware de renomeação de registradores é apenas uma das razões para limitar a largura do design a 4. (Bruce escreveu alguns posts de blog excelentes, como sua série sobre matemática FP e questões x87 / SSE / arredondamento , que eu faço altamente recomendado).


Nas CPUs da família AMD Bulldozer , move execuções mov immediate nas mesmas portas de execução inteira EX0 / EX1 que xor . mov reg,reg também pode ser executado no AGU0 / 1, mas isso é apenas para registrar a cópia, não para definir a partir de imediatos. Então AFAIK, na AMD, a única vantagem de xor over mov é a codificação mais curta. Também pode salvar resources de registro físico, mas não vi nenhum teste.


Idiomas de zeragem reconhecidos evitam penalidades de registro parcial nos processadores da Intel que renomeam os registradores parciais separadamente dos registradores completos (famílias P6 e SnB).

xor marcará o registrador como tendo as partes superiores zeradas , então xor eax, eax / inc al / inc eax evita a penalidade de registro parcial usual que os processadores pré-IvB possuem. Mesmo sem xor , o IvB só precisa de um uop de mesclagem quando os 8bits altos ( AH ) são modificados e, em seguida, o registro inteiro é lido, e Haswell até remove isso.

Do guia de microarcas da Agner Fog, pág. 98 (seção Pentium M, referenciada por seções posteriores incluindo SnB):

O processador reconhece o XOR de um registrador com ele mesmo como definindo-o para zero. Uma tag especial no registrador lembra que a parte alta do registrador é zero, então 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 

(de pg82): O processador lembra que os 24 bits superiores do EAX são zero, desde que você não receba uma interrupção, erro de interpretação ou outro evento de serialização.

pg82 desse guia também confirma que mov reg, 0 não é reconhecido como uma linguagem de zeramento, pelo menos nos primeiros projetos P6 como PIII ou PM. Eu ficaria muito surpreso se eles gastassem transistores em detectá-lo em CPUs posteriores.


xor define flags , o que significa que você precisa ter cuidado ao testar as condições. Como infelizmente o setcc está disponível apenas com um destino de 8 bits , você geralmente precisa tomar cuidado para evitar penalidades de registro parcial.

Seria legal se x86-64 redirecionasse um dos opcodes removidos (como o AAM) para um setcc setcc r/m 16/32/64 bits, com o predicado codificado no campo de 3 bits do registrador de origem do r / m campo (a maneira como algumas outras instruções de operandos únicos as usam como bits de opcode). Mas eles não fizeram isso, e isso não ajudaria em x86-32 de qualquer maneira.

Idealmente, você deve usar o registrador xor / set flags / setcc / read full:

 ... 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 ótimo desempenho em todas as CPUs (sem interrupções, operações de mesclagem ou dependencies falsas).

As coisas são mais complicadas quando você não quer xor antes de uma instrução de configuração de bandeira . Por exemplo, você quer se ramificar em uma condição e então setcc em outra condição a partir dos mesmos flags. por exemplo, cmp/jle , sete , e você não tem um registrador reserva, ou quer manter o xor fora do caminho do código não-ocupado completamente.

Não existem idiomas de zeragem reconhecidos que não afetam os sinalizadores, portanto, a melhor escolha depende da microarquitetura de destino. No Core2, inserir um uop de mesclagem pode causar uma paralisação de 2 ou 3 ciclos. Parece ser mais barato no SnB, mas não passei muito tempo tentando medir. Usando o mov reg, 0 / setcc teria uma penalidade significativa em processadores mais antigos da Intel, e ainda seria um pouco pior na Intel mais recente.

Usar setcc / movzx r32, r8 é provavelmente a melhor alternativa para as famílias Intel P6 e SnB, se você não puder xor-zero à frente das instruções de configuração de sinalizadores. Isso deve ser melhor do que repetir o teste depois de um xor-zeroing. (Nem considere sahf / lahf ou pushf / popf ). O IvB pode eliminar o movzx r32, r8 (ou seja, manipulá-lo com renomeação de registrador sem unidade de execução ou latência, como xor-zeroing). Haswell e, posteriormente, apenas eliminam instruções normais de mov , então movzx toma uma unidade de execução e tem latência diferente de zero, fazendo test / setcc / movzx pior que xor / test / setcc , mas ainda assim tão bom quanto test / mov r,0 / setcc (e muito melhor em CPUs mais antigas).

Usar setcc / movzx sem zerar primeiro é ruim em AMD / P4 / Silvermont, porque eles não rastreiam deps separadamente para sub-registros. Haveria um falso dep no valor antigo do registro. Usando mov reg, 0 / setcc para zerar / quebrar a dependência é provavelmente a melhor alternativa quando xor / test / setcc não é uma opção.

Claro, se você não precisa que a saída do setcc seja maior que 8 bits, você não precisa zerar nada. No entanto, cuidado com falsas dependencies em CPUs diferentes de P6 / SnB se você escolher um registro que foi recentemente parte de uma longa cadeia de dependencies. (E tome cuidado ao causar um particionamento parcial ou extra uop se você chamar uma function que possa salvar / restaurar o registro do qual você está usando parte).


and com um zero imediato não é especial-cased como independente do valor antigo em qualquer CPUs que eu conheço, por isso não quebra cadeias de dependência. Não tem vantagens sobre o xor e muitas desvantagens.

Veja http://agner.org/optimize/ para a documentação do microarch, incluindo quais idiomas de zeramento são reconhecidos como quebra de dependência (por exemplo, sub same,same está em algumas CPUs, mas nem todas, enquanto xor same,same é reconhecido em todos). quebra a cadeia de dependência no valor antigo do registrador (independentemente do valor de origem, zero ou não, porque é assim que o mov funciona). xor apenas interrompe as cadeias de dependencies no caso especial em que src e dest são o mesmo registrador, e é por isso que mov é deixado de fora da lista de separadores de dependência especialmente reconhecidos. (Além disso, porque não é reconhecido como um idioma de zeramento, com os outros benefícios que carrega.)

Curiosamente, o design P6 mais antigo (PPro) não reconheceu xor zeroing como um separador de dependência, apenas como uma linguagem de zeramento para evitar quebras de registro parcial, então, em alguns casos, valeu a pena usar ambos . (Veja o Exemplo 6.17 de Agner Fog. Em seu microarquivo pdf. Ele afirma que isso também se aplica a P2, P3, e até mesmo (cedo?) PM, mas eu sou cético sobre isso. Um comentário no post do blog diz que era apenas PPro que teve esse descuido. Parece realmente improvável que várias gerações da família P6 existissem sem reconhecer xor-zeroing como um depurador.)


Se isso realmente tornar seu código mais agradável ou salvar instruções, então certifique-se de zerar com mov para evitar tocar nos sinalizadores, contanto que você não introduza um problema de desempenho diferente do tamanho do código. Evitar bandeiras de ataque é a única razão sensata para não usar o xor .