Inicialmente, escribí este documento hace varios años, como un ingeniero de verificación de ejecución en ARM. Por supuesto, mi opinión fue influenciada por el trabajo en profundidad con los núcleos ejecutivos de diferentes procesadores. Así que hazlo con un descuento, por favor: tal vez soy demasiado categórico.
Sin embargo, todavía creo que los creadores de RISC-V podrían hacerlo mucho mejor. Por otro lado, si hubiera diseñado un procesador de 32 bits o 64 bits hoy, probablemente habría implementado tal arquitectura para aprovechar las herramientas existentes.
El artículo describió originalmente el conjunto de instrucciones RISC-V 2.0. Para la versión 2.2, realizó algunas actualizaciones.
Prólogo original: alguna opinión personal
El conjunto de instrucciones RISC-V se ha reducido a un mínimo absoluto. Se presta mucha atención a minimizar el número de instrucciones, normalizar la codificación, etc. Este deseo de minimalismo ha llevado a una ortogonalidad falsa (como reutilizar la misma instrucción para transiciones, llamadas y devoluciones) y verbosidad obligatoria, que infla el tamaño y la cantidad. instrucciones
Por ejemplo, aquí está el código C:
int readidx(int *p, size_t idx) { return p[idx]; }
Este es un caso simple de indexar una matriz, una operación muy común. Esta es la compilación para x86_64:
mov eax, [rdi+rsi*4] ret
o BRAZO:
ldr r0, [r0, r1, lsl #2] bx lr // return
Sin embargo, para RISC-V, se requiere el siguiente código:
slli a1, a1, 2 add a0, a1, a1 lw a0, a0, 0 jalr r0, r1, 0 // return
Simplificación RISC-V simplifica el decodificador (es decir, el front-end de la CPU) al ejecutar más instrucciones. Pero escalar el ancho de la tubería es un problema difícil, mientras que la decodificación de instrucciones irregulares ligeramente (o muy fuertes) está bien implementada (la principal dificultad surge cuando es difícil determinar la longitud de la instrucción: esto es especialmente evidente en el conjunto de instrucciones x86 con numerosos prefijos).
La simplificación del conjunto de instrucciones no debe llevarse al límite. El registro y la suma de registros con un cambio de la memoria de registro es una instrucción simple y muy común en los programas, y es muy fácil para el procesador implementarla de manera efectiva. Si el procesador no puede implementar la instrucción directamente, entonces puede ser relativamente fácil dividirla en sus componentes; Este es un problema mucho más simple que fusionar secuencias de operaciones simples.
Debemos distinguir entre las instrucciones específicas "complejas" de los procesadores CISC (instrucciones complicadas, poco utilizadas e ineficientes) de las instrucciones "funcionales" comunes a los procesadores CISC y RISC, que combinan una pequeña secuencia de operaciones. Estos últimos se utilizan con frecuencia y con alto rendimiento.
Implementación mediocre
- Extensibilidad casi ilimitada. Aunque este es el objetivo de RISC-V, crea un ecosistema fragmentado e incompatible que debe manejarse con extrema precaución.
- La misma instrucción (
JALR
) se usa para llamadas y para devoluciones, y para ramas indirectas de registro, donde se requiere decodificación adicional para la predicción de ramas
- Llamar:
Rd
= R1
- Retorno:
Rd
= R0
, Rs
= R1
- Transición indirecta:
Rd
= R0
, Rs
≠ R1
- (Transición extraña:
Rd
≠ R0
, Rd
≠ R1
)
- La codificación de longitud variable del campo de grabación no se sincroniza automáticamente (esto es común, por ejemplo, un problema similar con x86 y Thumb-2), pero esto causa varios problemas con la implementación y la seguridad, por ejemplo, la programación orientada hacia atrás, es decir, ataques ROP )
- RV64I requiere una extensión de caracteres para todos los valores de 32 bits. Esto lleva al hecho de que la mitad superior de los registros de 64 bits se vuelve imposible de usar para almacenar resultados intermedios, lo que lleva a una colocación especial innecesaria de la mitad superior de los registros. Es más óptimo usar la extensión con ceros (ya que reduce el número de conmutaciones y, por lo general, puede optimizarse rastreando el bit "cero" cuando se sabe que la mitad superior es cero)
- La multiplicación es opcional. Aunque los bloques de multiplicación rápida pueden ocupar un área bastante considerable en cristales diminutos, siempre puede usar circuitos ligeramente más lentos que usan activamente la ALU existente para múltiples ciclos de multiplicación.
LR
/ SC
requisitos de progresión estrictos para un subconjunto limitado de aplicaciones. Aunque esta restricción es bastante estricta, potencialmente crea algunos problemas para implementaciones pequeñas (especialmente sin un caché)
- Esto parece un reemplazo para la instrucción CAS, vea el comentario a continuación
- Los bits fijos de memoria FP y el modo de redondeo están en el mismo registro. Esto requiere la serialización del canal FP si la operación RMW se realiza para cambiar el modo de redondeo.
FP
instrucciones de FP
están codificadas para una precisión de 32, 64 y 128 bits, pero no de 16 bits (que es mucho más común en hardware que 128 bits)
- Esto se puede solucionar fácilmente: el código de dimensión
0b10
gratuito.
- Actualización: el marcador de posición decimal apareció en la versión 2.2, pero no hay un marcador de posición de media precisión. La mente es incomprensible.
- La forma en que se representan los valores FP en el archivo de registro FP no está definida, pero es observable (a través de carga / almacenamiento)
- Los autores del emulador te odiarán
- La migración de máquinas virtuales puede volverse imposible
- Actualización: la versión 2.2 requiere valores de boxeo NaN más amplios
Mal
- No hay códigos de condición, y en su lugar se utilizan las declaraciones de comparación y ramificación. Esto no es un problema en sí mismo, pero las consecuencias son desagradables:
- Espacio de codificación reducido en ramas condicionales debido a la necesidad de codificar uno o dos especificadores de registro
- Sin elección condicional (útil para transiciones muy impredecibles)
- Sin transferencia / sustracción con transferencia o préstamo
- (Tenga en cuenta que esto es aún mejor que los conjuntos de comandos que escriben banderas en el registro general y luego cambian a las banderas recibidas)
- Parece que se requieren contadores de alta precisión (ciclos de hardware) en un ISA sin privilegios. En la práctica, proporcionarles aplicaciones es un excelente vector para ataques en canales de terceros.
- La multiplicación y la división son parte de la misma extensión, y parece que si uno se implementa, el otro también debería serlo. La multiplicación es mucho más simple que la división, y es común en la mayoría de los procesadores, pero la división no lo es.
- No hay instrucciones atómicas en la arquitectura del conjunto de instrucciones básicas. Los microcontroladores multinúcleo se están volviendo más comunes, por lo que las instrucciones atómicas como LL / SC son económicas (para una implementación mínima dentro de un único procesador [multinúcleo], solo se necesita 1 bit de estado del procesador)
LR
/ SC
están en la misma extensión que las instrucciones atómicas más complejas, lo que limita la flexibilidad para implementaciones pequeñas
- Las instrucciones atómicas generales (no
LR
/ SC
) no incluyen primitiva CAS
- El
CmpHi:CmpLo
evitar la necesidad de una instrucción que lea cinco registros ( Addr
, CmpHi:CmpLo
, SwapHi:SwapLo
), pero esto probablemente impondrá menos sobrecarga de implementación que el LR
/ SC
reenvío garantizado, que se proporciona como reemplazos
- Se ofrecen instrucciones atómicas que funcionan en valores de 32 bits y 64 bits, pero no en valores de 8 o 16 bits.
- Para RV32I, no hay forma de transferir el valor DP FP entre un número entero y un archivo de registro FP, excepto a través de la memoria, es decir, desde registros enteros de 32 bits es imposible hacer un número de coma flotante de precisión doble de 64 bits, primero debe escribir el valor intermedio en la memoria y cargar él en el archivo de registro desde allí
- Por ejemplo, la instrucción
ADD
32 bits en RV32I y la ADD
64 bits en RVI64 tienen las mismas codificaciones, y en RVI64 se agrega otra codificación ADD.W
Esta es una complicación innecesaria para un procesador que implementa ambas instrucciones; sería preferible agregar una nueva codificación de 64 bits.
- No hay instrucciones
MOV
. El ensamblador traduce el código mnemónico del comando MV
a la instrucción MV rD, rS
-> ADDI rD, rS, 0
. Los procesadores de alto rendimiento generalmente optimizan las instrucciones MOV
, mientras reordenan ampliamente las instrucciones. Se eligió una instrucción con un operando directo de 12 bits como la forma canónica de la instrucción MV
en RISC-V.
- En ausencia de
MOV
la instrucción ADD rD, rS, r0
vuelve preferible al MOV
canónico, ya que es más fácil de decodificar, y las operaciones con registro cero (r0) en la CPU generalmente están optimizadas
Horrible
JAL
gasta 5 bits en la codificación del registro de comunicación, que siempre es igual a R1
(o R0
para las transiciones)
- Esto significa que el RV32I usa un desplazamiento de rama de 21 bits. Esto no es suficiente para aplicaciones grandes, por ejemplo, navegadores web, sin usar múltiples secuencias de comandos y / o "islas sucursales"
- ¡Esto es un deterioro en comparación con la versión 1.0 de la arquitectura de comandos!
- A pesar del gran esfuerzo para codificar uniformemente, las instrucciones de carga / almacenamiento se codifican de manera diferente (el caso y los campos inmediatos cambian)
- Aparentemente, la ortogonalidad de codificación del registro de salida era preferible a la ortogonalidad de codificación de dos instrucciones fuertemente relacionadas. Esta elección parece un poco extraña dado que la generación de direcciones es más crítica en el tiempo.
- No hay instrucciones de carga de memoria con compensaciones de registro (
Rbase
+ Roffset
) o índices ( Rbase
+ Rindex
<< Scale
).
FENCE.I
implica una sincronización completa del caché de instrucciones con todos los repositorios anteriores, con o sin vallado. Las implementaciones deben borrar todo I $ en la cerca o buscar D $ y el búfer de almacenamiento
- En RV32I, la lectura de contadores de 64 bits requiere leer la mitad superior dos veces, comparar y ramificar en el caso de transferir entre la mitad inferior y superior durante una operación de lectura
- Normalmente, los ISA de 32 bits incluyen una instrucción de registro de lectura de pares especiales para evitar este problema.
- No hay un espacio definido arquitectónicamente para la codificación de sugerencias, por lo que las instrucciones de este espacio no causan un error en los procesadores más antiguos (procesados como
NOP
), sino que hacen algo en las CPU más modernas
- Ejemplos típicos de pistas de NOP puro son cosas como el rendimiento de spinlock
- Los procesadores más nuevos también tienen pistas más sofisticadas (con efectos secundarios visibles en los procesadores más nuevos; por ejemplo, las instrucciones de verificación de bordes x86 están codificadas en el espacio de pistas para que los binarios sigan siendo compatibles con versiones anteriores)