El genio de los microprocesadores RISC-V

imagen


Las guerras entre RISC y CISC a fines de la década de 1990 se extinguieron hace mucho tiempo, y hoy se cree que la diferencia entre RISC y CISC es completamente irrelevante. Mucha gente afirma que los conjuntos de comandos son irrelevantes.



Sin embargo, los conjuntos de comandos son realmente importantes. Establecen restricciones sobre los tipos de optimizaciones que se pueden agregar fácilmente al microprocesador.



Recientemente, eché un vistazo más de cerca a la información sobre la arquitectura de conjunto de instrucciones (ISA) de RISC-V, y estos son algunos de los aspectos que realmente me impresionaron sobre el ISA de RISC-V:



  1. Es un conjunto de comandos RISC pequeño y fácil de aprender. Muy preferible para quienes estén interesados ​​en adquirir conocimientos sobre microprocesadores.
  2. , , .
  3. CPU ISA RISC-V.
  4. , , RISC-V.


RISC



Cuando comencé a comprender mejor RISC-V, me di cuenta de que RISC-V era un regreso radical a lo que muchos creían que era una era pasada de la informática. Desde el punto de vista de diseño, RISC-V en una máquina similar al tiempo de movimiento a una clásica R relucida I NSTRUCCIÓN S et C omputer (RISC, «ordenador con un conjunto de comandos cortas") primeras 80 y 90 años.



En los últimos años, muchos han argumentado que la división en RISC y CISC ya no tiene sentido, ya que se han agregado tantas instrucciones a los procesadores RISC como ARM, y si bien muchas de ellas son bastante complejas, que en la etapa actual es más un procesador híbrido que un procesador RISC puro. Se han aplicado consideraciones similares a otros procesadores RISC como el PowerPC.



RISC-V, por otro lado, es un representante verdaderamente "incondicional" de los procesadores RISC. Si lee acerca de las discusiones de RISC-V en Internet, encontrará personas que afirman que RISC-V fue desarrollado por algunos radicales de RISC de la vieja escuela que se niegan a mantenerse al día.



La exingeniera de ARM Erin Shepherd escribió una interesante crítica de RISC-V hace unos años :



ISA RISC-V . , .. (, , ) , .


Daré un pequeño contexto brevemente. El tamaño pequeño del código tiene una ventaja de rendimiento porque facilita el almacenamiento de código ejecutable dentro de la caché de alta velocidad del procesador.



La crítica aquí es que los diseñadores de RISC-V se han centrado demasiado en proporcionar un pequeño conjunto de instrucciones. Después de todo, este es uno de los objetivos originales de RISC.



Según Erin, la consecuencia de esto fue que un programa real necesitaría muchas más instrucciones para completar las tareas, es decir, ocuparía más espacio en la memoria.



Tradicionalmente, durante muchos años, se creyó que se debían agregar más instrucciones al procesador RISC para hacerlo más similar a CISC. La idea es que los comandos más especializados pueden reemplazar el uso de múltiples comandos comunes.



Compresión de comandos y fusión de macrooperaciones



Sin embargo, hay dos innovaciones en la arquitectura del procesador que hacen que esta estrategia de agregar instrucciones más complejas sea redundante de muchas maneras:



  • Instrucciones comprimidas: las instrucciones se comprimen en la memoria y se descomprimen en la primera etapa del procesador.
  • Fusión de macrooperación: el procesador lee dos o más instrucciones simples y las fusiona en una instrucción más compleja.


De hecho, ARM ya usa ambas estrategias y los procesadores x86 usan la última, por lo que RISC-V no hace ningún truco nuevo aquí.



Sin embargo, aquí hay una sutileza: RISC-V se beneficia de estas dos estrategias por dos razones importantes:



  1. Los comandos comprimidos se agregaron inicialmente. Otras arquitecturas, como ARM, pensaron en esto más tarde y las atornillaron de una manera bastante apresurada.
  2. Aquí es donde se justifica la obsesión de RISC con un pequeño número de equipos únicos. Simplemente queda más espacio para agregar comandos comprimidos.


El segundo punto requiere alguna aclaración. En las arquitecturas RISC, los comandos suelen tener 32 bits de ancho. Estos bits se deben utilizar para codificar información diversa. Digamos que tenemos un comando como este (hay comentarios después del punto y coma):



ADD x1, x4, x8    ; x1 ← x4 + x8
      
      





Agrega el contenido de los registros x4



y x8



almacena el resultado en x1



. El número de bits necesarios para codificar esta instrucción depende del número de registros disponibles. RISC-V y ARM tienen 32 registros. El número 32 se puede expresar en 5 bits:



2⁵ = 32


Dado que el comando necesita especificar tres registros diferentes, se requiere un total de 15 bits (3 × 5) para codificar los operandos (datos de entrada para la operación de suma).



Por lo tanto, cuantas más funciones queramos admitir en el conjunto de instrucciones, más bits sacaremos de los 32 bits disponibles. Por supuesto, podemos pasar a las instrucciones de 64 bits, pero esto consumirá demasiada memoria, lo que significa que el rendimiento se verá afectado.



En un esfuerzo agresivo por mantener pequeño el número de instrucciones, RISC-V deja más espacio para agregar bits para indicar que estamos usando instrucciones comprimidas. Si el procesador ve que ciertos bits están configurados en el comando, entonces entiende que debe interpretarse como comprimido.



Esto significa que en lugar de pegar 32 bits de una instrucción adentro, podemos colocar dos instrucciones de 16 bits de ancho cada una. Naturalmente, no todos los comandos RISC-V se pueden expresar en formato de 16 bits. Por lo tanto, se selecciona un subconjunto de instrucciones de 32 bits en función de su utilidad y frecuencia de uso. Si las instrucciones sin comprimir pueden recibir 3 operandos (datos de entrada), entonces las instrucciones comprimidas solo pueden recibir 2 operandos. Es decir, el comando comprimido ADD



se verá así:



C.ADD x4, x8     ; x4 ← x4 + x8
      
      





El código ensamblador RISC-V usa un prefijo C.



para indicar que el comando debe ensamblarse en un comando comprimido.



Esencialmente, las instrucciones comprimidas reducen el número de operandos. Tres registros de operandos tomarían 15 bits, ¡dejando solo 1 bit para indicar la operación! Por lo tanto, al usar dos operandos para indicar el código de operación (la operación a realizar), nos quedan 6 bits.



En realidad, esto se acerca a cómo funciona el ensamblador x86 cuando no se reservan suficientes bits para usar los tres registros de operandos. El procesador x86 desperdicia bits para permitir, por ejemplo, que el comando ADD



lea datos entrantes tanto de la memoria como de los registros.



Sin embargo, el verdaderonos beneficiamos de combinar la compresión de comandos con la fusión de macrooperaciones. Cuando el procesador recibe una palabra de 32 bits que contiene dos instrucciones comprimidas de 16 bits, puede fusionarlas en una instrucción más compleja.



Suena como una tontería, ¿hemos vuelto al punto de partida?



No, porque estamos evitando la necesidad de completar la especificación ISA con un montón de instrucciones complejas (es decir, la estrategia que sigue ARM). En cambio, estamos, en esencia, expresando una gran cantidad de comandos complejos de forma indirecta , a través de varias combinaciones de comandos simples.



En circunstancias normales, la macro-fusión causaría un problema: aunque dos instrucciones se reemplazan por una, aún ocupan el doble de memoria. Sin embargo, al comprimir comandos, no ocupamos ningún espacio extra. Aprovechamos ambas arquitecturas.



Veamos uno de los ejemplos dados por Erin Shepherd. En su artículo crítico sobre ISA RISC-V, muestra una función simple en C. Para hacerlo más claro, me tomé la libertad de reescribirlo:



int get_index(int *array, int i) { 
    return array[i];
}
      
      





En x86, esto se compilará con el siguiente código ensamblador:



mov eax, [rdi+rsi*4]
ret
      
      





Cuando se llama a una función en un lenguaje de programación, los argumentos generalmente se pasan a la función en un registro de acuerdo con un orden establecido que depende del conjunto de instrucciones utilizado. En x86, el primer argumento se coloca en un registro rdi



, el segundo en rsi



. De forma estándar, los valores devueltos deben colocarse en un registro eax



.



El primer comando multiplica el contenido rsi



por 4. Contiene una variable i



. ¿Por qué se multiplica? Porque array



consta de elementos enteros separados por 4 bytes. Por lo tanto, el tercer elemento de la matriz está en un desplazamiento de 3 × 4 = 12 bytes.



A continuación agregamos esto al rdi



que contiene la dirección base array



... Esto nos da la dirección final del i



elemento th array



. Leemos el contenido de la celda de memoria en esta dirección y lo almacenamos en eax



: tarea completada.



En ARM, todo sucede de manera similar:



LDR r0, [r0, r1, lsl #2]
BX  lr                    ;return
      
      





Aquí no estamos multiplicando por 4, sino desplazando el registro r1



2 bits a la izquierda, lo que equivale a multiplicar por 4. Esta es probablemente una descripción más precisa de lo que sucede en x86. Dudo que sea posible multiplicar por algo que no sea un múltiplo de 2, ya que la multiplicación es una operación bastante complicada y el cambio es económico y fácil.



De mi descripción de x86, el resto es una incógnita. Ahora vayamos a RISC-V, ¡donde comienza la verdadera diversión! (los comentarios comienzan con punto y coma)



SLLI a1, a1, 2     ; a1 ← a1 << 2
ADD  a0, a0, a1    ; a0 ← a0 + a1
LW   a0, a0, 0     ; a0 ← [a0 + 0]
RET
      
      





En RISC-V, los registros a0



y a1



son simplemente alias de x10



y x11



. Aquí es donde se colocan el primer y segundo argumento de la llamada a la función. RET



Es un pseudo-comando (taquigrafía):



JALR x0, 0(ra)     ; sp ← 0 + ra
                   ; x0 ← sp + 4  ignoring result
      
      





JALR



navega a la dirección almacenada en la ra



que se refiere a la dirección de retorno. ra



Es un seudónimo x1



.



Y todo se ve absolutamente terrible, ¿verdad? El doble de comandos para una operación simple y de uso común, como realizar una búsqueda de índice en una tabla y devolver un resultado



Realmente se ve mal. Es por eso que Erin Shepherd fue extremadamente crítica con las decisiones de diseño tomadas por los desarrolladores de RISC-V. Ella escribe:



Las simplificaciones de RISC-V simplifican el decodificador (es decir, el procesador frontal), pero se obtienen a costa de más instrucciones. Sin embargo, escalar el ancho de la tubería es una tarea complicada, mientras que la decodificación de algunas (o muy) instrucciones inusuales está bien estudiada (las principales dificultades surgen cuando la determinación de la longitud de una instrucción no es trivial; debido a sus infinitos prefijos, x86 es un caso particularmente descuidado).


Sin embargo, gracias a la compresión de comandos y la fusión de macro-operaciones, la situación se puede cambiar para mejor.



C.SLLI a1, 2      ; a1 ← a1 << 2
C.ADD  a0, a1     ; a0 ← a0 + a1
C.LW   a0, a0, 0  ; a0 ← [a0 + 0]
C.JR   ra
      
      





Ahora las instrucciones ocupan exactamente la misma cantidad de espacio de memoria que el ejemplo de ARM.



¡Bien, ahora hagamos la fusión Macro-op !



Una de las condiciones para que RISC-V permita fusionar operaciones en una es que el registro de destino coincida . Esta condición se cumple para los comandos ADD



y LW



(palabra de carga, "palabra de carga"). Por lo tanto, el procesador los convertirá en una instrucción.



Si se cumpliera esta condición para SLLI, entonces podríamos fusionar los tres comandos en uno . Es decir, el procesador vería algo que se asemeja a una instrucción ARM más compleja:



LDR r0, [r0, r1, lsl #2]
      
      





Pero, ¿por qué no pudimos escribir esta compleja operación macro directamente en el código?



¡Porque ISA no admite una operación de macro de este tipo! Recuerde que tenemos un número limitado de bits. ¡Entonces alarguemos los comandos! No, esto ocupará demasiada memoria y desbordará la preciosa caché del procesador más rápido.



Sin embargo, si en cambio emitimos estas instrucciones largas y semicomplejas dentro del procesador, entonces no surgen problemas. Un procesador nunca tiene más de unos pocos cientos de instrucciones al mismo tiempo. Por lo tanto, si gastamos en cada comando, digamos, 128 bits, esto no creará dificultades. Todavía habrá suficiente silicio para todo.



Cuando un decodificador recibe un comando ordinario, generalmente lo convierte en una o más microoperaciones. Estas microoperaciones son las instrucciones con las que realmente trabaja el procesador. Pueden ser muy amplios y contener mucha información adicional útil. El prefijo "micro" suena irónico, porque son más anchos. Sin embargo, en realidad, "micro" significa que tienen un número limitado de tareas.



La fusión de macrooperaciones pone el decodificador al revés: en lugar de convertir un comando en varias microoperaciones, tomamos muchas operaciones y las convertimos en una sola microoperación.



Es decir, lo que está sucediendo en un procesador moderno puede parecer bastante extraño:



  1. Primero, combina los dos equipos en uno usando compresión .
  2. Luego los divide en dos usando el desembalaje .
  3. Luego, los vuelve a combinar en una sola operación mediante la fusión macro- operativa .


Otros comandos pueden dividirse en varias microoperaciones y no fusionarse. ¿Por qué algunos equipos se fusionan y otros se dividen? ¿Hay un sistema en esta locura?



Un aspecto clave de la transición a las microoperaciones es el nivel de complejidad deseado:



  • No es demasiado complejo, porque de lo contrario no podrán completarse en un número fijo de ciclos de reloj asignados a cada comando.
  • No es demasiado simple, porque de lo contrario desperdiciaremos los recursos del procesador. Hacer dos microoperaciones llevará el doble de tiempo que hacer solo una.


Todo comenzó con los procesadores CISC. Intel comenzó a dividir sus complejas instrucciones CISC en microoperaciones para hacerlas más fáciles de encajar en las tuberías del procesador, como las instrucciones RISC. Sin embargo, en construcciones posteriores, los desarrolladores se dieron cuenta de que muchos equipos CISC podrían fusionarse en un equipo moderadamente complejo. Si hay menos comandos para ejecutar, el trabajo se completará más rápido.



Beneficios obtenidos



Hemos hablado de muchos detalles, por lo que ahora debe resultarle difícil comprender cuál es el significado de todo este trabajo. ¿Para qué sirve toda esta compresión y fusión? Parece que están haciendo mucho trabajo innecesario.



Primero, la compresión de comandos no se parece en nada a la compresión zip. La palabra "compresión" es un poco engañosa porque la compresión o descompresión instantánea de un comando es completamente simple. No se pierde el tiempo en esto.



Lo mismo se aplica a la fusión de macrooperaciones. Aunque el proceso puede parecer complicado, ya se utilizan sistemas similares en microprocesadores modernos. Por tanto, los costes a los que se suma toda esta complejidad ya se han pagado.



Sin embargo, a diferencia de los diseñadores de ARM, MIPS y x86, cuando comenzaron a diseñar su ISA, los creadores de RISC-V conocían la compresión de comandos y la fusión de macrooperaciones. A través de varias pruebas con el primer conjunto mínimo de instrucciones, hicieron dos descubrimientos importantes:



  1. Los programas RISC-V suelen ocupar aproximadamente el mismo o menos espacio de memoria que cualquier otra arquitectura de procesador. Incluido x86, que debería utilizar la memoria de manera eficiente, dado que es ISA CISC.
  2. Necesita realizar menos microoperaciones que otras ISA.


De hecho, al diseñar el conjunto de instrucciones base teniendo en cuenta la fusión, pudieron fusionar suficientes instrucciones para que el procesador de cualquier programa tuviera que realizar menos microoperaciones que los procesadores de la competencia.



Esto llevó al equipo de desarrollo de RISC-V a redoblar sus esfuerzos para implementar la fusión de macrooperaciones como una estrategia fundamental de RISC-V. El manual de RISC-V tiene muchas notas sobre las operaciones con las que puede fusionarse. También incluye algunas correcciones para facilitar la combinación de comandos que se encuentran en patrones comunes.



Small ISA facilita el aprendizaje de los estudiantes. Esto significa que es más fácil para un estudiante de arquitectura de procesadores diseñar su propio procesador ejecutándose en instrucciones RISC-V. Vale la pena recordar que tanto la compresión de comandos como la fusión de macro-operaciones son opcionales.



RISC-V tiene un pequeño conjunto de comandos fundamentales que se deben implementar. Sin embargo, todos los demás comandos se implementan como parte de las extensiones. Los comandos comprimidos son solo una extensión opcional.



La fusión macro-operativa es solo optimización. No cambia el comportamiento en general y, por lo tanto, no es necesario implementarlo en su propio procesador RISC-V.



Estrategia de diseño RISC-V



RISC-V tomó todo lo que sabemos sobre los procesadores modernos en la actualidad y utilizó ese conocimiento para diseñar procesadores ISA. Por ejemplo, sabemos que:



  • Los núcleos de procesador actuales tienen un sofisticado sistema de predicción de ramas.
  • Los núcleos del procesador son superescalares, es decir, ejecutan muchas instrucciones en paralelo.
  • Para garantizar la superescalaridad, se utiliza la ejecución fuera de orden.
  • Tienen transportadores.


Esto significa que las funciones como la ejecución condicional compatible con ARM ya no son necesarias. El soporte de ARM para esta función elimina bits del formato de instrucción. RISC-V puede guardar estos bits.



La ejecución condicional se diseñó originalmente para evitar bifurcaciones, porque son malas para las canalizaciones. Para acelerar el trabajo del procesador, generalmente recibe los siguientes comandos de antemano, de modo que inmediatamente después de que se haya ejecutado el anterior, en la primera etapa del procesador, se pueda recoger el siguiente.



Con la bifurcación condicional, no podemos saber de antemano dónde estará el próximo comando cuando comencemos a llenar la tubería. Sin embargo, un procesador superescalar puede simplemente ejecutar ambas ramas en paralelo.



Es por esto que RISV-C tampoco tiene registros de estado, porque crean dependencias entre comandos. Cuanto más independiente sea cada comando, más fácil será ejecutarlo en paralelo con otro comando.



Básicamente, la estrategia RISC-V es que podemos hacer que ISA sea lo más simple posible y que la implementación mínima de un procesador RISC-V sea lo más simple posible sin la necesidad de decisiones de diseño que harían imposible la creación de un procesador de alto rendimiento.






Publicidad



Nuestra empresa ofrece servidores no solo con CPU Intel, sino también servidores con procesadores AMD EPYC. Al igual que con otros tipos de servidores, existe una gran selección de sistemas operativos para la instalación automática, es posible instalar cualquier sistema operativo desde su propia imagen. ¡Pruebalo ahora!






All Articles