Por que as instruções x86-64 em registros de 32 bits zeram a parte superior do registro de 64 bits completo?
No Tour do x86-64 dos manuais da Intel , li
Talvez o fato mais surpreendente seja que uma instrução como essa MOV EAX, EBXzera automaticamente os 32 bits superiores do RAXregistro.
A documentação da Intel (3.4.1.1 Registros de uso geral no modo de 64 bits no manual Arquitetura básica) citada na mesma fonte nos diz:
Operandos de 64 bits geram um resultado de 64 bits no registrador de uso geral de destino.
Operandos de 32 bits geram um resultado de 32 bits, estendido por zero para um resultado de 64 bits no registrador de uso geral de destino.
Operandos de 8 e 16 bits geram um resultado de 8 ou 16 bits. Os 56 bits ou 48 bits superiores (respectivamente) do registrador de uso geral de destino não são modificados pela operação. Se o resultado de uma operação de 8 ou 16 bits se destinar ao cálculo do endereço de 64 bits, estenda explicitamente o registro para 64 bits completos.
Em montagem x86-32 e x86-64, instruções de 16 bits, como
não mostre esse tipo de comportamento "estranho" de que a palavra superior de eax seja zerada.
Assim: qual é o motivo pelo qual esse comportamento foi introduzido? À primeira vista, parece ilógico (mas o motivo pode ser que estou acostumado com as peculiaridades do assembly x86-32).
assembly x86 x86-64 cpu-registers zero-extension
— Nubok
Se você pesquisar por "Parcial registro estolado" no Google, você encontrará muitas informações sobre o problema que eles estavam (quase certamente) tentando evitar.
Não apenas "mais". AFAIK, todas as instruções com um r32operando de destino zeram o 32 alto, em vez de mesclar. Por exemplo, alguns montadores substituirão pmovmskb r64, xmmpor pmovmskb r32, xmm, salvando um REX, porque a versão de destino de 64 bits se comporta de forma idêntica. Embora a seção Operação do manual liste todas as 6 combinações de 32/64 bits dest e 64/128 / 256b fonte separadamente, a extensão zero implícita da forma r32 duplica a extensão zero explícita da forma r64. Estou curioso sobre a implementação do HW ...
@HansPassant, a referência circular começa.
— kchoi
Relacionado: xor eax,eaxou xor r8d,r8dé a melhor maneira de zerar RAX ou R8 (salvar um prefixo REX para RAX, e XOR de 64 bits não é nem tratado especialmente no Silvermont). Relacionado: Como exatamente os registros parciais no Haswell / Skylake funcionam? A escrita AL parece ter uma falsa dependência de RAX e AH é inconsistente
Não sou AMD ou falando por eles, mas teria feito da mesma forma. Porque zerar a metade superior não cria uma dependência do valor anterior, que a CPU teria que esperar. O mecanismo de renomeação de registro seria essencialmente derrotado se não fosse feito dessa forma.
Dessa forma, você pode escrever código rápido usando valores de 32 bits no modo de 64 bits sem ter que quebrar as dependências explicitamente o tempo todo. Sem esse comportamento, cada instrução de 32 bits no modo de 64 bits teria que esperar por algo que aconteceu antes, mesmo que essa parte alta quase nunca fosse usada. (Fazer int64 bits desperdiçaria espaço de cache e largura de banda de memória; x86-64 suporta de forma mais eficiente tamanhos de operando de 32 e 64 bits )
O comportamento para tamanhos de operando de 8 e 16 bits é estranho. A loucura da dependência é uma das razões pelas quais as instruções de 16 bits são evitadas agora. O x86-64 herdou isso de 8086 para 8 bits e 386 para 16 bits, e decidiu fazer os registradores de 8 e 16 bits funcionarem da mesma maneira no modo de 64 bits e no modo de 32 bits.
Veja também Por que o GCC não usa registros parciais? para detalhes práticos de como as gravações em registros parciais de 8 e 16 bits (e leituras subsequentes do registro completo) são tratadas por CPUs reais.
Não acho estranho, acho que eles não quiseram quebrar muito e mantiveram o velho comportamento aí.
— Alexey Frunze
@Alex quando eles introduziram o modo 32 bits, não havia um comportamento antigo para a parte alta. Não havia parte alta antes. É claro que depois disso não poderia mais ser alterado.
Eu estava falando sobre operandos de 16 bits, por que os bits superiores não são zerados nesse caso. Eles não o fazem em modos diferentes de 64 bits. E isso também é mantido no modo de 64 bits.
Eu interpretei seu "O comportamento das instruções de 16 bits é estranho" como "é estranho que a extensão zero não aconteça com operandos de 16 bits no modo de 64 bits". Daí meus comentários sobre como mantê-lo da mesma forma no modo de 64 bits para melhor compatibilidade.
@Alex oh, entendo. Está bem. Não acho estranho dessa perspectiva. Apenas de uma perspectiva "olhando para trás, talvez não tenha sido uma ideia tão boa". Acho que deveria ter sido mais claro :)
Ele simplesmente economiza espaço nas instruções e no conjunto de instruções. Você pode mover pequenos valores imediatos para um registro de 64 bits usando as instruções existentes (32 bits).
Também evita que você tenha que codificar valores de 8 bytes para MOV RAX, 42quando MOV EAX, 42puder ser reutilizado.
Essa otimização não é tão importante para operações de 8 e 16 bits (porque são menores) e alterar as regras também quebraria o código antigo.
Se estiver correto, não faria mais sentido estender o sinal em vez de estender 0?
A extensão do sinal é mais lenta, mesmo no hardware. A extensão zero pode ser feita em paralelo com qualquer cálculo que produza a metade inferior, mas a extensão do sinal não pode ser feita até (pelo menos o sinal de) a metade inferior ter sido calculada.
Outro truque relacionado é usar XOR EAX, EAXporque XOR RAX, RAXprecisaria de um prefixo REX.
@Nubok: Claro, eles poderiam ter adicionado uma codificação de movzx / movsx que leva um argumento imediato. Na maioria das vezes, é mais conveniente zerar os bits superiores, então você pode usar um valor como um índice de array (porque todos os regs devem ter o mesmo tamanho em um endereço efetivo: [rsi + edx]não é permitido). Obviamente, evitar falsas dependências / paralisações de registros parciais (a outra resposta) é outro motivo importante.
e alterar as regras também quebraria o código antigo. O código antigo não pode ser executado no modo de 64 bits (por exemplo, 1 byte inc / dec são prefixos REX); isso é irrelevante. O motivo para não limpar os defeitos do x86 são as menos diferenças entre o modo longo e os modos de compatibilidade / legado, portanto, menos instruções precisam ser decodificadas de maneira diferente dependendo do modo. A AMD não sabia que o AMD64 iria pegar e, infelizmente, era muito conservador, então levaria menos transistores para suportar. A longo prazo, não teria problema se compiladores e humanos tivessem que lembrar quais coisas funcionam de maneira diferente no modo de 64 bits.
Sem zero estendendo-se para 64 bits, isso significaria que uma instrução de leitura raxteria 2 dependências para seu raxoperando (a instrução que escreve eaxe a instrução que escreve raxantes dela), isso significa que 1) o ROB teria que ter entradas para múltiplas dependências para um único operando, o que significa que o ROB exigiria mais lógica e transistores e ocuparia mais espaço, e a execução seria mais lenta aguardando uma segunda dependência desnecessária que poderia levar anos para ser executada; ou alternativamente 2), que suponho que aconteça com as instruções de 16 bits, o estágio de alocação provavelmente para (ou seja, se o RAT tiver uma alocação ativa para uma axgravação e uma eaxleitura aparecer, ele para até que a axgravação seja retirada).
mov eax, 7 //retires before add rax, 6
mov rdx, rax // has to wait for both imul rax, rdx and mov eax, 7 to finish before dispatch to the execution units, even though the higher order bits are identical anyway
O único benefício de extensão diferente de zero é garantir que os bits de ordem superior raxsejam incluídos, por exemplo, se originalmente contiver 0xffffffffffffffff, o resultado seria 0xffffffff00000007, mas há muito pouca razão para o ISA fazer essa garantia com tal despesa, e é mais provável que o benefício da extensão zero seja realmente mais necessário, portanto, ele economiza a linha de código extra mov rax, 0. Garantindo que ele sempre será estendido de zero a 64 bits, os compiladores podem trabalhar com este axioma em mente enquanto estão ligados mov rdx, rax, raxapenas tem que esperar por sua dependência única, o que significa que pode começar a execução mais rápido e se retirar, liberando unidades de execução. Além disso, também permite expressões idiomáticas zero mais eficientes, como xor eax, eaxzero, raxsem exigir um byte REX.
Sinalizadores parciais no Skylake pelo menos funcionam por ter entradas separadas para CF e qualquer um de SPAZO. (Então cmovbeé 2 uops, mas cmovbé 1). Mas nenhuma CPU que renomeie o registro parcial o faz da maneira que você sugeriu. Em vez disso, eles inserem um uop mesclado se um reg parcial for renomeado separadamente do reg completo (ou seja, for "sujo"). Veja Por que o GCC não usa registros parciais? e como exatamente os registros parciais no Haswell / Skylake funcionam? A escrita AL parece ter uma falsa dependência de RAX, e AH é inconsistente
As CPUs da família P6 travaram por ~ 3 ciclos para inserir um uop de fusão (Core2 / Nehalem), ou a família P6 anterior (PM, PIII, PII, PPro) travaram por (pelo menos?) ~ 6 ciclos. Talvez seja como você sugeriu em 2, aguardando que o valor de registro completo esteja disponível por meio de write-back no arquivo de registro permanente / arquitetônico.
@PeterCordes oh, eu sabia sobre a fusão de uops pelo menos para tendas parciais de sinalização. Faz sentido, mas esqueci como funciona por um minuto; clicou uma vez, mas esqueci de fazer anotações
@PeterCordes microarchitecture.pdf: This gives a delay of 5 - 6 clocks. The reason is that a temporary register has been assigned to AL to make it independent of AH. The execution unit has to wait until the write to AL has retired before it is possible to combine the value from AL with the value of the rest of EAXNão consigo encontrar um exemplo do 'uop de fusão' que seria usado para resolver isso, o mesmo para uma paralisação parcial de sinalização
Certo, o P6 inicial apenas para até o write-back. Core2 e Nehalem inserem um uop de fusão antes / depois? apenas retardando o front-end por um tempo mais curto. Sandybridge insere uops de fusão sem parar. (Mas a fusão AH precisa emitir em um ciclo por si só, enquanto a fusão AL pode fazer parte de um grupo completo.) Haswell / SKL não renomeia AL separadamente de RAX, então mov al, [mem]é uma carga microfundida + ALU- merge, apenas renomeando AH, e um uop de fusão AH ainda apresenta problemas sozinho. Os mecanismos de mesclagem de flag parcial nesses CPUs variam, por exemplo, Core2 / Nehalem ainda está travado para flags parciais, ao contrário de parcial-reg.