Teste se um registro é zero com reg CMP, 0 vs OR reg, reg?

Existe alguma diferença de velocidade de execução usando o seguinte código:

cmp al, 0 je done 

e o seguinte:

 or al, al jz done 

Eu sei que as instruções JE e JZ são as mesmas, e também que usar OR dá uma melhoria de tamanho de um byte. No entanto, também estou preocupado com a velocidade do código. Parece que os operadores lógicos serão mais rápidos que um SUB ou um CMP, mas eu só queria ter certeza. Isso pode ser um trade-off entre tamanho e velocidade, ou um ganha-ganha (claro que o código será mais opaco).

Depende da seqüência exata do código, qual é a CPU específica e outros fatores.

O principal problema com or al, al, é que ele “modifica” o EAX , o que significa que uma instrução subseqüente que usa o EAX de alguma forma pode parar até que essa instrução seja concluída. Observe que a ramificação condicional ( jz ) também depende da instrução, mas os fabricantes de CPU fazem muito trabalho (predição de ramificação e execução especulativa) para atenuar isso. Observe também que, em teoria, seria possível para um fabricante de CPU projetar uma CPU que reconheça que o EAX não seja alterado neste caso específico, mas existem centenas desses casos especiais e os benefícios de reconhecer a maioria deles são muito pequenos.

O principal problema com cmp al,0 é que ele é um pouco maior, o que pode significar uma busca de instrução mais lenta / mais pressão de cache e (se for um loop) pode significar que o código não se encheckbox mais no “buffer de loop” de uma CPU.

Como Jester apontou nos comentários; test al,al evita os dois problemas – é menor que cmp al,0 e não modifica o EAX .

É claro (dependendo da seqüência específica) o valor em AL deve ter vindo de algum lugar, e se veio de uma instrução que defina sinalizadores adequadamente, pode ser possível modificar o código para evitar usar outra instrução para definir sinalizadores novamente mais tarde.

Sim , há uma diferença no desempenho.

A melhor escolha para comparar um registro com zero no x86 moderno é o test reg, reg (se o ZF ainda não estiver definido apropriadamente pela instrução que set reg ). É como AND reg,reg mas sem escrever o destino.

or reg,reg não pode macro-fuse, adiciona latência para qualquer coisa que ler depois, e precisa de um novo registro físico para manter o resultado. (Assim, ele usa resources de renomeação de registradores onde o test não funcionaria, limitando a janela de instruções fora de ordem da CPU ). (Reescrevendo o dst pode ser uma vitória na família Intel P6, no entanto, veja abaixo.)


Os resultados do sinalizador test reg,reg do test reg,reg / and reg,reg / or reg,reg são idênticos ao cmp reg, 0 em todos os casos (exceto para AF):

  • CF = OF = 0 porque test / and sempre faça isso, e para cmp porque subtrair zero não pode transbordar ou carregar.
  • ZF , SF , PF definido de acordo com o resultado (ou seja, reg ): reg&reg para teste ou reg - 0 para cmp. Assim, você pode testar números inteiros negativos ou não assinados com o bit alto definido pelo SF.

    Ou com jl , porque OF = 0, então a condição l ( SF!=OF ) é equivalente a SF . Cada CPU que pode macro-fuse TEST / JL também pode macro-fuse TEST / JS, mesmo Core2. Mas depois do CMP byte [mem],0 , sempre use JL não JS para ramificar no bit de sinal.

( AF é indefinido após o test , mas definido de acordo com o resultado para cmp . Eu o estou ignorando porque é realmente obscuro: os únicos consumidores para AF são as instruções de BCD com ajuste ASCII como AAS e lahf / pushf .)


test é mais curto para codificar que cmp com 0 imediato, em todos os casos exceto o caso especial cmp al, imm8 que ainda é dois bytes. Mesmo assim, o test é preferível por motivos de macro-fusão (e similares no Core2), e porque não ter nada imediato pode possivelmente ajudar a economizar a densidade do cache, deixando um espaço que outra instrução pode pedir se precisar de mais espaço (SnB -família).


Os decodificadores nos processadores Intel e AMD podem internamente test macro e cmp com algumas instruções de ramificação condicional em uma única operação de comparação e ramificação. Isso dá a você um throughput máximo de 5 instruções por ciclo quando a macro-fusão acontece, contra 4 sem macro-fusão. (Para processadores Intel desde o Core2.)

CPUs Intel recentes podem macro-fuse algumas instruções (como and e add / sub ), bem como test e cmp , mas or não é um deles. Os processadores AMD só podem mesclar test e cmp com um JCC. Veja as condições x86_64 – Assembly – loop e fora de ordem , ou apenas se refira diretamente aos documentos de microarcas da Agner Fog para os detalhes de qual CPU pode macro-fuse. test pode macro-fusível em alguns casos onde o cmp não pode, por exemplo, com js .

Quase todos os ops simples da ALU (bitolde booleano, add / sub, etc.) são executados em um único ciclo. Todos eles têm o mesmo “custo” de rastreá-los através do pipeline de execução fora de ordem. Intel e AMD gastam os transistores para fazer unidades de execução rápida para adicionar / sub / qualquer coisa em um único ciclo. Sim, OR ou bit a bit é mais simples e provavelmente usa menos energia, mas ainda não pode ser executado mais rápido que um ciclo de clock.


Além disso, como aponta Brendan, or reg, reg adiciona outro ciclo de latência à cadeia de dependencies para seguir instruções que precisam ler o registro.

No entanto, nas CPUs da família P6 (PPro / PII para Nehalem), a gravação do registro de destino pode realmente ser uma vantagem . Há um número limitado de portas de leitura de registro para o estágio de problema / renomeação para ler o arquivo de registro permanente, mas os valores recentemente gravados estão disponíveis diretamente do ROB. Reescrever um registro desnecessariamente pode torná-lo ativo na rede de encaminhamento novamente para ajudar a evitar as baias de leitura de registros. (Veja o microarca de Agner Fog pdf .

O compilador do Delphi supostamente usa or eax,eax , o que era uma escolha razoável na época, assumindo que as baias de leitura de registros eram mais importantes do que alongar a cadeia dep para qualquer coisa que fosse lida a seguir.

Infelizmente, os compiladores-escritores da época não sabiam o futuro, porque and eax,eax executa exatamente de forma equivalente a or eax,eax na família Intel P6, mas é menos ruim em outras áreas porque pode and macro-fuse em Sandybridge. família.

Para Core2 / Nehalem (os dois últimos uarches da família P6), o test pode macro-fusível, mas não pode, portanto (ao contrário do Pentium II / III / M) é um trade-off entre macro-fusão e possivelmente redução de registro. leia baias. A evasão register-read-stall ainda ocorre ao custo de latência extra se o valor for lido depois de ser testado, portanto o test pode ser uma escolha melhor do que and em alguns casos antes mesmo de um cmov ou setcc , não um jcc ou em CPUs sem macro-fusão.

Se você estiver ajustando algo para ser rápido em vários uarches, use test menos que o perfil mostre que as baias de leitura de registro são um grande problema em um caso específico no Core2 / Nehalem, e usando and realmente o corrige.

IDK onde o or reg,reg idiom veio, exceto talvez que seja mais curto para digitar. Ou talvez tenha sido usado propositalmente para que os processadores P6 reescrevessem um registro deliberadamente antes de usá-lo um pouco mais. Coders na época não podiam prever que acabaria sendo menos eficiente do que and para esse propósito. Mas obviamente nunca devemos usá-lo em test ou em novos códigos. (Há apenas uma diferença quando é imediatamente antes de um jcc na família Sandybridge, mas é mais simples esquecer or reg,reg .)


Para testar um valor na memory , não há problema em cmp dword [mem], 0 , mas os processadores da Intel não podem usar instruções de configuração de sinalizador de macro-fusíveis que tenham um operando imediato e de memory. Se você for usar o valor após a comparação em um lado da ramificação, provavelmente deverá mov eax, [mem] / test eax,eax ou algo assim. Se não (por exemplo, testando um booleano), cmp com um operando de memory está bem.

Embora note que alguns modos de endereçamento não micro-fusionarão na família SnB : o RIP-relative + immediate não irá se fundir nos decodificadores, ou os modos de endereçamento indexados serão desinflados. De qualquer forma, levando a 3 uops de domínio fundido para cmp dword [rsi + rcx*4], 0 / jne ou [rel some_static_location] .

Você também pode testar um valor na memory com o test dword [mem], -1 , mas não o faça. Como o test r/m16/32/64, sign-extended-imm8 não está disponível, é pior do que o tamanho do código cmp para algo maior que bytes. (Eu acho que a idéia do projeto era que se você quisesse testar apenas o bit baixo de um registrador, apenas test cl, 1 invés do test ecx, 1 , e casos de uso como o test ecx, 0xfffffff0 são raros o bastante vale a pena gastar um opcode.Especialmente desde que essa decisão foi feita para 8086 com código de 16 bits, onde era apenas a diferença entre um imm8 e imm16, não imm32.)

Eu escrevi -1 em vez de 0xFFFFFFFF, então seria o mesmo com byte ou qword . ~0 seria outra maneira de escrevê-lo.