¿Por qué MIPS usa R0 como "cero" cuando podría simplemente XOR dos registros para producir 0?

10

Creo que estoy buscando una respuesta a una pregunta de trivia. Estoy tratando de entender por qué la arquitectura MIPS usa un valor "cero" explícito en un registro cuando puede lograr lo mismo simplemente haciendo XOR de cualquier registro contra sí mismo. Se podría decir que la operación ya está hecha para usted; sin embargo, realmente no puedo imaginar una situación en la que usarías muchos valores "cero". Leí los documentos originales de Hennessey, y simplemente asigna un cero de hecho sin ninguna justificación real.

¿Existe una razón lógica para tener una asignación binaria codificada de cero?

Actualización: en 8k de un ejecutable de xc32-gcc para el núcleo MIPS en el PIC32MZ, tengo una sola instancia de "cero".

add     t3,t1,zero

la respuesta real: otorgé la recompensa a la persona que tenía la información sobre MIPS y los códigos de condición. La respuesta en realidad radica en la arquitectura MIPS para las condiciones. Aunque inicialmente no quería asignar tiempo a esto, revisé la arquitectura para opensparc , MIPS-V y OpenPOWER (este documento era interno) y aquí están los resultados resumidos. El registro R0 es necesario para la comparación en ramas debido a la arquitectura de la tubería.

  • comparación de enteros contra cero y rama (bgez, bgtz, blez, bltz)
  • entero compara dos registros y rama (beq, bne)
  • entero compara dos registros y trap (teq, tge, tlt, tne)
  • entero comparar registro e inmediato y trampa (teqi, tgei, tlti, tnei)

Simplemente se reduce a cómo se ve el hardware en la implementación. Del manual de MIPS-V, hay una cita sin referencia en la página 68:

Las ramas condicionales fueron diseñadas para incluir operaciones de comparación aritmética entre dos registros (como también se hace en PA-RISC y Xtensa ISA), en lugar de usar códigos de condición (x86, ARM, SPARC, PowerPC), o para comparar solo un registro contra cero ( Alfa, MIPS), o dos registros solo para igualdad (MIPS). Este diseño fue motivado por la observación de que una instrucción combinada de comparación y ramificación se convierte en una tubería regular, evita el estado del código de condición adicional o el uso de un registro temporal, y reduce el tamaño del código estático y el seguimiento de búsqueda de instrucciones dinámicas. Otro punto es que las comparaciones contra cero requieren un retraso de circuito no trivial (especialmente después del cambio a la lógica estática en procesos avanzados) y, por lo tanto, son casi tan caras como la magnitud aritmética. Otra ventaja de una instrucción fusionada de comparar y bifurcar es que las bifurcaciones se observan antes en el flujo de instrucciones de front-end y, por lo tanto, se pueden predecir antes. Quizás haya una ventaja en un diseño con códigos de condición en el caso de que se puedan tomar varias ramas en función de los mismos códigos de condición, pero creemos que este caso es relativamente raro.

El documento MIPS-V no afecta al autor de la sección citada. Agradezco a todos por su tiempo y consideración.

b degnan
fuente
66
A menudo desea utilizar un registro con valor 0 en alguna operación como valor fuente. Sería una sobrecarga poner a cero un registro antes de esas operaciones, por lo que el rendimiento se beneficia si solo puede usar un cero proporcionado en lugar de crearlo usted mismo cuando sea necesario. Los ejemplos incluyen la adición de una bandera de acarreo.
JimmyB
3
En la arquitectura AVR, gcc se encarga de inicializar r1 a cero al inicio y nunca toca ese valor nuevamente, usando r1 como fuente donde no se puede usar un 0 inmediato. Aquí, el compilador 'emula' el registro cero dedicado en software por razones de rendimiento. (La mayoría de los AVR tienen 32 registros, por lo que reservar uno (dos, en realidad) a un lado no cuesta mucho en relación con los posibles beneficios de rendimiento y tamaño del código.)
JimmyB
1
No sé acerca de MIPS, pero puede ser más rápido mover r0 a otro registro en comparación con XORing ese registro para borrarlo.
JimmyB
¿Entonces no está de acuerdo con el punto de que el cero es tan frecuente que merece una posición en el archivo de registro? Entonces, probablemente tenga razón porque es cierto, esto es controvertido y hay muchas ISA que eligen no reservar un registro cero. Al igual que otras características controvertidas en ese momento, como ventanas de registro, ranuras de sucursales, predicaciones de instrucciones de "los viejos tiempos" ... si va a diseñar un ISA, no tiene que usarlas si decide no hacerlo.
user3528438
2
Puede ser interesante leer uno de los viejos documentos RISC de Berkeley, RISC I: A Reduced Instruction Set VLSI Computer . Muestra cómo el uso de un registro cero cableado, R0, permite que se implementen varias instrucciones VAX y modos de direccionamiento en una sola instrucción RISC.
Mark Plotnick

Respuestas:

14

El registro cero en las CPU RISC es útil por dos razones:

Es una constante útil

Dependiendo de las restricciones de la ISA, no puede usar un literal en la codificación de algunas instrucciones, pero puede estar seguro de que puede usarlo r0para obtener 0.

Se puede usar para sintetizar otras instrucciones

Este es quizás el punto más importante. Como diseñador de ISA, puede intercambiar un registro de propósito general con un registro cero para poder sintetizar otras instrucciones útiles. Sintetizar instrucciones es bueno porque al tener menos instrucciones reales, necesita menos bits para codificar una operación en un código de operación, lo que libera espacio en el espacio de codificación de instrucciones. Puede usar ese espacio para tener, por ejemplo, compensaciones de dirección más grandes y / o literales.

La semántica del registro cero es como /dev/zeroen los sistemas * nix: todo lo escrito en él se descarta y siempre se lee 0.

Veamos algunos ejemplos de cómo podemos hacer pseudoinstrucciones con la ayuda del r0registro cero:

; ### Hypothetical CPU ###

; Assembler with syntax:
; op rd, rm, rn 
; => rd: destination, rm: 1st operand, rn: 2nd operand
; literal as #lit

; On an CPU architecture with a status register (which contains arithmetic status
; flags), `sub` can be used, with r0 as destination to discard result.
cmp rn, rm     ; => sub r0, rn, rm

; `add` instruction can be used as a `mov` instruction:
mov rd, rm     ; => add rd, rm, r0
mov rd, #lit   ; => add rd, r0, #lit

; Negate:
neg rd, rm     ; => sub rd, r0, rm

; On CPU without status flags,
nop            ; => add r0, r0, r0

; RISC-V's `jal` instruction -- Jump and Link: Jump to PC-relative instruction,
; save return address into rd; we can synthesize a `jmp` instruction out of it.
jmp dest       ; => jal r0, dest

; You can even load from an absolute (direct) address, for a usually small range
; of addresses by using a literal offset as an address.
ld rd, addr    ; => ld rd, [r0, #addr]

El caso de MIPS

Miré más de cerca el conjunto de instrucciones MIPS. Hay un puñado de pseudoinstrucciones que usa $zero; Se utilizan principalmente para las ramas. Aquí hay algunos ejemplos de lo que he encontrado:

move $rt, $rs          => add $rt, $rs, $zero

not $rt, $rs           => nor $rt, $rs, $zero

b Label                => beq $zero, $zero, Label ; a small relative branch

bgt $rs, $rt, Label    => slt $at, $rt, $rs
                          bne $at, $zero, Label

blt $rs, $rt, Label    => slt $at, $rs, $rt
                          bne $at, $zero, Label

bge $rs, $rt, Label    => slt $at, $rs, $rt
                          beq $at, $zero, Label

ble $rs, $rt, Label    => slt $at, $rt, $rs
                          beq $at, $zero, Label

En cuanto a por qué ha encontrado solo una instancia del $zeroregistro en su desensamblaje, tal vez sea su desensamblador lo suficientemente inteligente como para transformar secuencias conocidas de instrucciones en su pseudoinstrucción equivalente.

¿Es realmente útil el registro cero ?

Bueno, aparentemente, ARM considera que tener un registro cero es lo suficientemente útil como para que en su (algo) nuevo núcleo ARMv8-A, que implementa AArch64, ahora haya un registro cero en modo de 64 bits; no había un registro cero antes. (Sin embargo, el registro es un poco especial, en algunos contextos de codificación es un registro cero, en otros designa el puntero de la pila )

Jarhmander
fuente
No creo que MIPS use banderas, ¿verdad? El registro cero agrega la capacidad de leer / escribir incondicionalmente ciertas direcciones sin tener en cuenta el contenido de los registros de la CPU y ayuda a facilitar una operación de estilo "mov inmediato", pero otros movimientos podrían hacerse al ordenar lógicamente la fuente. .
supercat
1
De hecho, no existe un registro que sostienen banderas aritméticas, en cambio hay tres instrucciones que bifurcaciones condicionales ayuda emular comunes ( slt, slti, sltu).
Jarhmander
Mirando el conjunto de instrucciones MIPS, y dado que entiendo que cada instrucción se obtendrá para cuando se ejecute la instrucción anterior, me pregunto si hubiera sido difícil tener un código de operación que no sea directamente a nada, pero en cambio digo que si se ejecuta una instrucción en modo inmediato y la siguiente instrucción recuperada tiene ese patrón de bits, ¿se tomarán los 16 bits superiores del operando de la instrucción obtenida previamente? Eso haría que las operaciones en modo inmediato de 32 bits se manejaran con una instrucción de dos palabras y dos ciclos en lugar de tener que gastar dos palabras y dos ciclos ...
supercat
... cargando un operando y luego un tercer ciclo para usarlo realmente.
supercat
7

La mayoría de las implementaciones ARM / POWER / SPARC tienen un registro RAZ oculto

Puede pensar que ARM32, SPARC, etc. no tienen un registro 0, ¡pero de hecho lo tienen! A nivel de microarquitectura, la mayoría de los ingenieros de diseño de CPU agregan un registro 0 que puede ser invisible para el software (el registro cero de ARM es invisible) y usan ese registro cero para simplificar la decodificación de instrucciones.

Considere un diseño ARM32 moderno típico que tiene un registro invisible de software, digamos R16 conectado a 0. Considere la carga ARM32, muchos casos de instrucción de carga ARM32 caen en una de estas formas (ignore la indexación previa y posterior por un tiempo para mantener la discusión simple ) ...

LDR ra, [rb] // NOTE:The ! is optional and represents address writeback.
LDR ra, [rb, rc](!)
LDR ra, [rb, #k](!)

Dentro del procesador, esto decodifica a un general

ldr.uop ra, rb, rx, rc, #c // Internal decoded instruction format.

antes de entrar en la etapa de emisión donde se leen los registros. Tenga en cuenta que rx representa el registro para reescribir la dirección actualizada. Aquí hay algunos ejemplos de decodificación:

LDR R0, [R1]      ==> ldr.uop R0, R1, R16, R16, #0 // Writeback to NULL. 
LDR R0, [R1, R2]! ==> ldr.uop R0, R1, R1, R2,   #0 // Writeback to R1.
LDR R0, [R1, #2]  ==> ldr.uop R0, R1, R16, R16, #2 // Writeback to NULL.

A nivel de circuito, las tres cargas son en realidad la misma instrucción interna y una manera fácil de obtener este tipo de ortogonalidad es crear un registro de tierra R16. Dado que R16 siempre está conectado a tierra, estas instrucciones naturalmente se decodifican correctamente sin ninguna lógica adicional. La asignación de una clase de instrucciones a un único formato interno ayuda enormemente en las implementaciones superescalares, ya que reduce la complejidad lógica.

Otra razón es una forma simplificada de tirar las escrituras. Las instrucciones pueden deshabilitarse simplemente configurando el registro de destino y las banderas en R16. No es necesario crear ninguna otra señal de control para desactivar la reescritura, etc.

La mayoría de las implementaciones de procesador, independientemente de la arquitectura, terminan con un modelo de registro RAZ al principio de la tubería. La tubería MIPS esencialmente comienza en un punto que en otras arquitecturas sería de algunas etapas.

MIPS tomó la decisión correcta

Por lo tanto, un registro de lectura como cero es casi obligatorio en cualquier implementación de procesador moderna y MIPS que lo hace visible para el software es definitivamente un punto positivo dado cómo simplifica la lógica de decodificación interna. Los diseñadores de procesadores MIPS no necesitan agregar un registro RAZ adicional ya que $ 0 ya está en el suelo. Dado que RAZ está disponible para el ensamblador, MIPS tiene muchas instrucciones de psuedo disponibles y uno puede pensar que esto empuja parte de la lógica de decodificación al ensamblador en lugar de crear formatos dedicados para cada tipo de instrucción para ocultar el registro RAZ del software como con otras arquitecturas. El registro RAZ es una buena idea y por eso ARMv8 lo copió.

Si ARM32 tuviera un registro de $ 0, la lógica de decodificación se habría simplificado y la arquitectura habría sido mucho mejor en términos de velocidad, área y potencia. Por ejemplo, de las tres versiones de LDR presentadas anteriormente, solo se necesitarían 2 formatos. Del mismo modo, no es necesario reservar la lógica de decodificación para las instrucciones MOV y MVN. Además, CMP / CMN / TST / TEQ se volvería redundante. Tampoco sería necesario diferenciar entre la multiplicación corta (MUL) y la multiplicación larga (UMULL / SMULL), ya que la multiplicación corta podría considerarse una multiplicación larga con el registro alto establecido en $ 0, etc.

Dado que MIPS fue diseñado inicialmente por un pequeño equipo, la simplicidad del diseño era importante y, por lo tanto, $ 0 se eligió explícitamente en el espíritu de RISC. ARM32 conserva muchas características tradicionales de CISC a nivel arquitectónico.

Revanth Kamaraj
fuente
1
No todas las CPU ARM32 funcionan de la manera que usted describe. Algunos tienen un rendimiento inferior para instrucciones de carga más complejas y / o para reescribir en el registro. Por lo tanto, no todos pueden decodificar exactamente de la misma manera.
Peter Cordes
6

Descargo de responsabilidad: Realmente no conozco el ensamblador MIPS, pero el registro de valor 0 no es exclusivo de esta arquitectura, y supongo que se usa de la misma manera que en otras arquitecturas RISC que conozco.

XORing un registro para obtener 0 le costará una instrucción, mientras que el uso de un registro de valor 0 predefinido no lo hará.

Por ejemplo, la mov RX, RYinstrucción a menudo se implementa como add RX, RY, R0. Sin un registro de valor 0, tendría que hacerlo xor RZ, RZcada vez que quiera usar mov.

Otro ejemplo es la cmpinstrucción y sus variantes (como "comparar y saltar", "comparar y mover", etc.), donde cmp RX, R0se usa para probar números negativos.

Dmitry Grigoryev
fuente
1
¿Habría algún problema de aplicación MOV Rx,Rycomo AND Rx,Ry,Ry?
supercat
3
@supercat No podrá codificar mov RX, Immo mov RX, mem[RY]si su conjunto de instrucciones solo admite un único valor inmediato y un único acceso a la memoria por instrucción.
Dmitry Grigoryev
No estoy familiarizado con los modos de direccionamiento que tiene el MIPS. Sé que el ARM tiene modos [Rx + Ry << scale] y [Rx + disp], y aunque poder usar este último para algunas direcciones absolutas podría ser útil en algunos casos, generalmente no es esencial. Se podría emular un modo [Rx] directo a través de [Rx + disp] utilizando desplazamiento cero. ¿Qué usa el MIPS?
supercat
moves un mal ejemplo; puede implementarlo con un 0 inmediato en lugar de un registro cero. por ej ori dst, src, 0. Pero sí, necesitaría un código de operación para que mov-inmediato se registre si no lo hubiera hecho addiu $dst, $zero, 1234, luipero para los 16 bits inferiores en lugar de los 16 superiores. Y no podría usar noro subconstruir un operando no / neg .
Peter Cordes
@supercat: en caso de que todavía te preguntes: MIPS clásico solo tiene un modo de direccionamiento único: registro + disp16. El MIPS moderno agregó otros códigos de operación para modos de direccionamiento de 2 registros para cargas / almacenes de FP, acelerando la indexación de la matriz. (Pero aún no es para la carga / almacenamiento de enteros, tal vez porque eso podría requerir más puertos de lectura en el archivo de registro de enteros para 2 registros de direcciones + un registro de datos para una tienda. Consulte Uso de un registro como compensación )
Peter Cordes,
3

Atar algunos cables a tierra al final de su banco de registro es barato (más barato que convertirlo en un registro completo).

Hacer el xor real toma un poco de energía y tiempo para cambiar las puertas y luego almacenarlo en el registro, ¿por qué pagar ese costo cuando un valor 0 existente puede estar fácilmente disponible?

Los cpus modernos también tienen un registro de valor 0 (oculto) que pueden usar como resultado de una xor eax eaxinstrucción a través del cambio de nombre del registro.

monstruo de trinquete
fuente
66
El costo real R0no está en conectar a tierra algunos cables, sino en el hecho de que tiene que reservar un código para él en cada instrucción que se ocupa de los registros.
Dmitry Grigoryev
El xor es un arenque rojo. xor-zeroing solo es bueno en x86, donde las CPU reconocen el idioma y evitan la dependencia de las entradas. Como señalas, Sandybridge-family ni siquiera ejecuta una operación para ello, solo lo maneja en la etapa de cambio de nombre de registro. ( ¿Cuál es la mejor manera de establecer un registro en cero en el ensamblado x86: xor, mov o y? ). Pero en MIPS, XORing un registro tendría una dependencia falsa; Las reglas de ordenamiento de dependencia de memoria (HW equivalente de C ++ std::memory_order_consume) requieren que XOR propague la dependencia.
Peter Cordes
Si no tuviera un registro cero, incluiría un código de operación para mover un inmediato a un registro. Me gusta, luipero no desplazado a la izquierda por 16. Entonces todavía puede poner un número pequeño en un registro con una sola instrucción. Permitir solo cero con una dependencia falsa sería una locura. (El MIPS normal crea valores distintos de cero con addiu $dst, $zero, 1234o ori, por lo que su argumento de "costo de energía" se desglosa. Si desea evitar activar una ALU, debe incluir un código de operación para que mov-inmediatamente se registre en lugar de tener el software ADD u OR un inmediato con cero.)
Peter Cordes