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
performance
assembly
optimization
x86
micro-optimization
balajimc55
fonte
fonte
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 usexor 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, usexor r10d,r10d
, notxor r10,r10
.Exemplos de inteiros GP:
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 menospxor
, masxorps
precisa da porta 5 de execução no Intel Nehalem, enquantopxor
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
epxor
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
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 / AVXvpcmpeqd
é uma quebra de dep em muitos (embora ainda precise de um uop para escrever os 1s), mas AVX512vpternlogd
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.
O que há de especial em zerar expressões idiomáticas como xor em vários uarches
Algumas CPUs reconhecem
sub same,same
como um idioma de zeragemxor
, mas todas as CPUs que reconhecem qualquerxor
idioma de zeragem o reconhecem . Use apenasxor
para não precisar se preocupar com qual CPU reconhece qual idioma de zeragem.xor
(sendo um idioma zeroing reconhecido, ao contráriomov 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 quexor
.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 axor
maismov
é 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ãoxor eax, eax
/inc al
/inc eax
evita a penalidade usual de registro parcial que as CPUs pré-IvB têm. Mesmo semxor
, 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):
A pág82 desse guia também confirma que não
mov 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: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 oxor
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 consideresahf
/lahf
oupushf
/popf
). O IvB pode eliminarmovzx 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 eliminammov
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 quexor
/ 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. Usarmov reg, 0
/setcc
para zerar / quebrar a dependência é provavelmente a melhor alternativa quandoxor
/ 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 vantagensxor
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, enquantoxor 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 quemov
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 qualmov
é 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 ambosmov
e entãoxor
- 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.TL: DR:
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 usarxor
, mas às vezes você pode xou-zero antes do que define os sinalizadores se você tiver um registrador sobressalente.mov
-zero à frentesetcc
é melhor para latência do quemovzx reg32, reg8
depois (exceto na Intel quando você pode escolher registros diferentes), mas pior tamanho de código.fonte
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 depopcnt/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ê dizxor 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.