¿Por qué las instrucciones x86-64 en registros de 32 bits ponen a cero la parte superior del registro completo de 64 bits?

118

En el Tour x86-64 de los manuales de Intel , leí

Quizás el hecho más sorprendente es que una instrucción como MOV EAX, EBXautomáticamente pone a cero los 32 bits superiores del RAXregistro.

La documentación de Intel (3.4.1.1 Registros de propósito general en modo de 64 bits en la arquitectura básica manual) citada en la misma fuente nos dice:

  • Los operandos de 64 bits generan un resultado de 64 bits en el registro de propósito general de destino.
  • Los operandos de 32 bits generan un resultado de 32 bits, ampliado a cero a un resultado de 64 bits en el registro de propósito general de destino.
  • Los operandos de 8 y 16 bits generan un resultado de 8 o 16 bits. Los 56 bits o 48 bits superiores (respectivamente) del registro de propósito general de destino no son modificados por la operación. Si el resultado de una operación de 8 bits o de 16 bits está destinado al cálculo de direcciones de 64 bits, extienda explícitamente el signo del registro a los 64 bits completos.

En el ensamblaje x86-32 y x86-64, instrucciones de 16 bits como

mov ax, bx

no muestre este tipo de comportamiento "extraño" de que la palabra superior de eax se ponga a cero.

Entonces: ¿cuál es la razón por la que se introdujo este comportamiento? A primera vista, parece ilógico (pero la razón podría ser que estoy acostumbrado a las peculiaridades del ensamblaje x86-32).

Nubok
fuente
16
Si busca en Google "Parada de registro parcial", encontrará bastante información sobre el problema que (casi con toda seguridad) estaban tratando de evitar.
Jerry Coffin
4
No solo "la mayoría". AFAIK, todas las instrucciones con un r32operando de destino ponen a cero el 32 alto, en lugar de fusionarse. Por ejemplo, algunos ensambladores reemplazarán pmovmskb r64, xmmcon pmovmskb r32, xmm, guardando un REX, porque la versión de destino de 64 bits se comporta de manera idéntica. Aunque la sección Operación del manual enumera las 6 combinaciones de fuente de 32 / 64bit dest y 64/128 / 256b por separado, la extensión cero implícita del formulario r32 duplica la extensión cero explícita del formulario r64. Tengo curiosidad por la implementación de HW ...
Peter Cordes
2
@HansPassant, comienza la referencia circular.
kchoi
1
Relacionado: xor eax,eaxo xor r8d,r8des la mejor manera de poner a cero RAX o R8 (guardar un prefijo REX para RAX, y XOR de 64 bits ni siquiera se maneja especialmente en Silvermont). Relacionado: ¿Cómo funcionan exactamente los registros parciales en Haswell / Skylake? Escribir AL parece tener una falsa dependencia de RAX, y AH es inconsistente
Peter Cordes

Respuestas:

97

No soy AMD ni hablo por ellos, pero lo habría hecho de la misma manera. Debido a que poner a cero la mitad alta no crea una dependencia del valor anterior, la CPU tendría que esperar. El mecanismo de cambio de nombre de registros esencialmente se anularía si no se hiciera de esa manera.

De esta manera, puede escribir código rápido utilizando valores de 32 bits en modo de 64 bits sin tener que romper explícitamente las dependencias todo el tiempo. Sin este comportamiento, cada instrucción de 32 bits en modo de 64 bits tendría que esperar a algo que sucedió antes, aunque esa parte alta casi nunca se usaría. (Hacer int64 bits desperdiciaría la huella de caché y el ancho de banda de la memoria; x86-64 admite tamaños de operandos de 32 y 64 bits de manera más eficiente )

El comportamiento de los operandos de 8 y 16 bits es extraño. La locura de la dependencia es una de las razones por las que ahora se evitan las instrucciones de 16 bits. x86-64 heredó esto de 8086 para 8 bits y 386 para 16 bits, y decidió que los registros de 8 y 16 bits funcionaran de la misma manera en el modo de 64 bits que en el modo de 32 bits.


Consulte también ¿Por qué GCC no usa registros parciales? para obtener detalles prácticos sobre cómo las escrituras en registros parciales de 8 y 16 bits (y lecturas posteriores del registro completo) son manejadas por CPU reales.

harold
fuente
8
No creo que sea extraño, creo que no querían romper demasiado y mantuvieron el antiguo comportamiento allí.
Alexey Frunze
5
@Alex cuando introdujeron el modo de 32 bits, no había un comportamiento antiguo para la parte alta. No había una parte alta antes ... Por supuesto que después de eso no se pudo cambiar más.
harold
1
Estaba hablando de operandos de 16 bits, por qué los bits superiores no se ponen a cero en ese caso. No lo hacen en modos que no son de 64 bits. Y eso también se mantiene en modo de 64 bits.
Alexey Frunze
3
Interpreté su "El comportamiento de las instrucciones de 16 bits es extraño" como "es extraño que la extensión cero no ocurra con operandos de 16 bits en el modo de 64 bits". De ahí mis comentarios sobre mantenerlo de la misma manera en modo de 64 bits para una mejor compatibilidad.
Alexey Frunze
8
@ Alex oh ya veo. Okay. No creo que sea extraño desde esa perspectiva. Solo desde una perspectiva de "mirar atrás, tal vez no fue una buena idea". Supongo que debería haber sido más claro :)
harold
9

Simplemente ahorra espacio en las instrucciones y el conjunto de instrucciones. Puede mover pequeños valores inmediatos a un registro de 64 bits mediante las instrucciones existentes (32 bits).

También le evita tener que codificar valores de 8 bytes para MOV RAX, 42cuándo MOV EAX, 42se pueden reutilizar.

Esta optimización no es tan importante para operaciones de 8 y 16 bits (porque son más pequeñas), y cambiar las reglas allí también rompería el código antiguo.

Bo Persson
fuente
7
Si eso es correcto, ¿no habría tenido más sentido firmar-extender en lugar de 0 extender?
Damien_The_Unbeliever
16
La extensión de la señal es más lenta, incluso en hardware. La extensión cero se puede hacer en paralelo con cualquier cálculo que produzca la mitad inferior, pero la extensión del signo no se puede hacer hasta que (al menos el signo de) la mitad inferior se haya calculado.
Jerry Coffin
13
Otro truco relacionado es usarlo XOR EAX, EAXporque XOR RAX, RAXnecesitaría un prefijo REX.
Neil
3
@Nubok: Claro, podrían haber agregado una codificación de movzx / movsx que toma un argumento inmediato. La mayoría de las veces es más conveniente tener los bits superiores puestos a cero, por lo que puede usar un valor como índice de matriz (porque todas las reglas deben tener el mismo tamaño en una dirección efectiva: [rsi + edx]no está permitido). Por supuesto, evitar falsas dependencias / bloqueos de registros parciales (la otra respuesta) es otra razón importante.
Peter Cordes
4
y cambiar las reglas allí también rompería el código antiguo. El código antiguo no se puede ejecutar en modo de 64 bits de todos modos (por ejemplo, inc / dec de 1 byte son prefijos REX); esto es irrelevante. La razón para no limpiar las verrugas de x86 es que hay menos diferencias entre el modo largo y los modos compat / legacy, por lo que hay menos instrucciones para decodificar de manera diferente según el modo. AMD no sabía que AMD64 iba a ponerse de moda y, lamentablemente, era muy conservador, por lo que se necesitarían menos transistores para admitirlo. A largo plazo, habría estado bien si los compiladores y los humanos tuvieran que recordar qué cosas funcionan de manera diferente en el modo de 64 bits.
Peter Cordes
1

Sin que el cero se extienda a 64 bits, significaría que una instrucción que lee raxtendría 2 dependencias para su raxoperando (la instrucción que escribe eaxy la instrucción que escribe raxantes), esto significa que 1) el ROB debería tener entradas para múltiples dependencias para un solo operando, lo que significa que el ROB requeriría más lógica y transistores y ocuparía más espacio, y la ejecución sería más lenta esperando una segunda dependencia innecesaria que podría tardar años en ejecutarse; o alternativamente 2), que supongo que sucede con las instrucciones de 16 bits, la etapa de asignación probablemente se detiene (es decir, si la RAT tiene una asignación activa para una axescritura y eaxaparece una lectura, se detiene hasta que la axescritura se retira).

mov rdx, 1
mov rax, 6
imul rax, rdx
mov rbx, rax
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

El único beneficio de no extenderse a cero es garantizar que raxse incluyan los bits de orden superior de , por ejemplo, si originalmente contiene 0xffffffffffffffff, el resultado sería 0xffffffff00000007, pero hay muy pocas razones para que la ISA haga esta garantía a tal costo, y es más probable que se requiera más el beneficio de la extensión cero, por lo que ahorra la línea adicional de código mov rax, 0. Al garantizar que siempre será cero extendido a 64 bits, los compiladores pueden trabajar con este axioma en mente mientras están dentro mov rdx, rax, raxsolo tienen que esperar su dependencia única, lo que significa que puede comenzar la ejecución más rápido y retirarse, liberando unidades de ejecución. Además, también permite modismos cero más eficientes, como xor eax, eaxa cero, raxsin requerir un byte REX.

Lewis Kelsey
fuente
Las banderas parciales en Skylake al menos funcionan al tener entradas separadas para CF frente a cualquiera de SPAZO. (Entonces cmovbees 2 uops pero cmovbes 1). Pero ninguna CPU que cambie el nombre de un registro parcial lo hace de la manera que usted sugiere. En su lugar, insertan un uop de fusión si se cambia el nombre de un registro parcial por separado del registro completo (es decir, está "sucio"). Consulte ¿Por qué GCC no usa registros parciales? y ¿Cómo funcionan exactamente los registros parciales en Haswell / Skylake? Escribir AL parece tener una falsa dependencia de RAX, y AH es inconsistente
Peter Cordes
Las CPU de la familia P6 se ​​detuvieron durante ~ 3 ciclos para insertar una uop fusionada (Core2 / Nehalem), o la familia P6 anterior (PM, PIII, PII, PPro) simplemente se estancó durante (¿al menos?) ~ 6 ciclos. Tal vez sea como sugirió en 2, esperando que el valor de registro completo esté disponible mediante escritura en el archivo de registro permanente / arquitectónico.
Peter Cordes
@PeterCordes oh, sabía sobre la fusión de uops al menos para puestos de bandera parciales. Tiene sentido, pero olvidé cómo funciona por un minuto; Hizo clic una vez pero olvidé tomar notas
Lewis Kelsey
@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 EAXNo puedo encontrar un ejemplo de la 'fusión de uop' que se usaría para resolver esto, sin embargo, lo mismo para un estancamiento parcial de la bandera
Lewis Kelsey
Bien, el P6 temprano simplemente se detiene hasta que se escribe. Core2 y Nehalem insertan un uop de fusión después / antes? solo parando el front-end por un tiempo más corto. Sandybridge inserta fusionando uops sin atascarse. (Pero la combinación de AH tiene que producirse en un ciclo por sí misma, mientras que la combinación de AL puede ser parte de un grupo completo). Haswell / SKL no cambia el nombre de AL por separado de RAX, por lo que mov al, [mem]es una carga microfundida + ALU- fusionar, solo cambiar el nombre de AH, y un uop de combinación de AH todavía se emite solo. Los mecanismos de fusión de banderas parciales en estas CPU varían, por ejemplo, Core2 / Nehalem todavía se detiene para las banderas parciales, a diferencia del registro parcial.
Peter Cordes