Qual é a melhor maneira de definir um registrador como zero no assembly x86: xor, mov ou e?

Respostas:

222

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,r32como 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, xmmpode fazer sentido. É um byte a menos pxor, mas xorpsprecisa da porta 5 de execução no Intel Nehalem, enquanto pxorpode 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, xorpse pxorsã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, ymmnã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..7máscara AVX512 . SSE / AVX vpcmpeqdé uma quebra de dep em muitos (embora ainda precise de um uop para escrever os 1s), mas AVX512 vpternlogdpara 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.


O que há de especial em zerar expressões idiomáticas como xor em vários uarches

Algumas CPUs reconhecem sub same,samecomo um idioma de zeragem xor, mas todas as CPUs que reconhecem qualquerxor idioma de zeragem o reconhecem . Use apenas xorpara 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):

  • 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 utiliza unidade de execução, economizando energia e liberando recursos de execução. (Família SnB da Intel)
  • uop menor (sem dados imediatos) deixa espaço na linha de cache uop para instruções próximas para emprestar, se necessário. (Família SnB da Intel).
  • não usa entradas no arquivo de registro físico . (Família SnB Intel (e P4) pelo menos, possivelmente AMD também, uma vez que eles usam um design PRF semelhante em vez de manter o estado de registro no ROB como microarquitetura da família Intel P6.)

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 immediateroda nas mesmas portas de execução de inteiros EX0 / EX1 que xor. mov reg,regtambé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 xormais 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).

xorirá marcar o registro como tendo as partes superiores zeradas , então xor eax, eax/ inc al/ inc eaxevita 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.


xordefine sinalizadores , o que significa que você deve ter cuidado ao testar as condições. Uma vez que, setccinfelizmente, 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, setee você não quer ter um registo de reposição, ou você quer manter o xorpara 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/ setccteria 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/ lahfou 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 movinstruções regulares , portanto, movzxleva uma unidade de execução e tem latência diferente de zero, tornando o teste / setcc/ movzxpior 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/ movzxsem 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/ setccpara zerar / quebrar a dependência é provavelmente a melhor alternativa quando xor/ test / setccnão é uma opção.

Obviamente, se você não precisa que setcca 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.)


andcom 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 xore 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.) movQuebra a cadeia de dependência do valor antigo do registro (independente do valor da fonte, zero ou não, pois é assim que movfunciona). xorsomente 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 imulcadeia de ligação . Isso confirma os resultados de Agner Fog, infelizmente.


TL: DR:

Se isso realmente torna seu código mais agradável ou salva instruções, então com certeza, zere com movpara 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, reg8depois (exceto na Intel quando você pode escolher registros diferentes), mas pior tamanho de código.

Peter Cordes
fonte
7
A maioria das instruções aritméticas OP R, S são forçadas por uma CPU desordenada a esperar que o conteúdo do registro R seja preenchido por instruções anteriores com o registro R como destino; esta é uma dependência de dados. O ponto principal é que os chips Intel / AMD têm hardware especial para quebrar dependências obrigatórias de espera por dados no registro R quando XOR R, R é encontrado, e não necessariamente para outras instruções de zeramento de registro. Isso significa que a instrução XOR pode ser agendada para execução imediata e é por isso que a Intel / AMD recomenda seu uso.
Ira Baxter
3
@IraBaxter: Sim, e apenas para evitar qualquer confusão (porque eu vi esse equívoco no SO), mov reg, srctambé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/tzcntter uma dependência falsa no destino)
Peter Cordes
2
@Zboson: A "latência" de uma instrução sem dependências só importa se houver uma bolha no pipeline. É bom para a eliminação mov, mas para zerar as instruções, o benefício da latência zero só entra em ação depois de algo como um erro de previsão do branch ou I $ miss, onde a execução está esperando pelas instruções decodificadas, em vez de os dados estarem prontos. Mas sim, a eliminação de mov não movliberta, 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.
Peter Cordes
2
De acordo com Agner, o KNL não reconhece a independência de registradores de 64 bits. Portanto 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.
Bóson Z de
3
ah, cadê o bom e velho MIPS, com seu "registro zero" quando você precisa dele.
hayalci