RISC-V: introducción a la ISA – Parte 1

 

Durante la pausa sobre la serie LPIC-1, publicaré este artículo que será el primero de la serie RISC-V  (pronunciado como «risk-five») sobre esta interesante ISA abierta. En esta serie se tratará el tema de la arquitectura de computadoras desde cero y será vital para entender mejor toda la serie de programación que comenzaré también en breve. Por el momento, en este primer artículo solo quiero introducir términos, y poco a poco en los siguientes veremos cómo se diseña una ISA, tipos de instrucciones, microarquitecturas, etc.

¿Por qué RISC-V? Bueno, esto es sencillo de responder. Simplemente porque se trata de una ISA open-source bajo licencia BSD. Al ser de código abierto tiene una gran cantidad de documentación sobre todos los detalles técnicos que se puede estudiar e incluso existen algunas microarquitecturas ya implementadas que también son abiertas para poderlas analizar como modelos de estudio.

Durante esos 15 años que intenté aprender sobre todo esto, y crear la enciclopedia sobre microprocesadores que publiqué (que en un inicio iba a ser simplemente unos apuntes para mi), me he dado cuenta que te puedes encontrar con personas de todo tipo. A las malas personas mejor no dedicar un segundo a hablar de ellas, pero podría escribir un libro sobre las puertas cerradas y los impedimentos que ponen algunos. En cambio, sí que me gustaría dedicar un tiempo para agradecer la ayuda de Manuel Ujaldón (Universidad de Málaga) y de Julio Ortega Lopera (Universidad de Granada), entre otros…

¿Qué es una instrucción?

Código ensamblador (ejemplo)

Una instrucción no es más que un segmento de código que contiene implícita una operación que la CPU debe realizar. Por ejemplo, una instrucción add, al ser extraída de la memoria cuando se está ejecutando un programa que la contiene y llega a la CPU, la unidad de control la debe interpretar y generará una serie de señales de control para mandar a las diferentes unidades funcionales lo que deben hacer. En este caso, le diría a la ALU que sume dos datos almacenados…

Ejecutando las instrucciones que contiene un código de forma ordenada y secuencial se consigue ejecutar el programa. Aunque eso de forma secuencial y en orden ya veremos en futuros artículos que se puede saltar…

Una ISA contendrán un determinado número de instrucciones (mov, add, sub, jmp, not, and, xor,…) que el programador tiene disponibles para crear códigos en ensamblador o que el compilador puede usar para traducir esos lenguajes de alto nivel en un código de más bajo nivel compuesto por este tipo de instrucciones. Lo veremos en la serie sobre programación con ejemplos prácticos.

¡Nunca! ¡Nunca! ¡Nunca! ¡Jamás! Aprendas lenguaje ensamblador x86 para iniciarte en los lenguajes ASM. El repertorio de Intel es una auténtica porquería y extremadamente malo para iniciarte. Es una recomendación personal. Es preferible que comiences con otros, como por ejemplo ASM para ARM…

Aunque en un código ensamblador o ASM puedas ver esas instrucciones como mnemónicos, en la memoria no se almacenan así, ni tampoco llegan como tal a la CPU. Se almacenan en formato binario, es decir, compuestas por códigos compuestos por unos y ceros. Eso es debido a que la electrónica no entiende de ensamblador, ni siquiera de unos y ceros, sino de señales eléctricas bajas o altas, que será realmente lo que se interpreta como unos y ceros.

La forma en que se codifican o cómo están compuestas las instrucciones las dejo para futuros artículos. Ahora simplemente quiero que captes los conceptos.

Esa información de instrucciones y datos en formato binario es lo que se conoce como código máquina. Ese código se puede representar de forma binaria o, también en ocasiones, de forma hexadecimal. Eso sí, tanto el lenguaje ensamblador como el código máquina son dependientes de la ISA o de la arquitectura para la que han sido escritos. Por ejemplo, un chip ARM no entendería un programa o código escrito en ASM de x86 aunque lo traduzcamos a formato binario…

; Ejemplo de Hola Mundo en ARMv7 (32-bit)

.text            
.global _start
_start:
    mov r0, #1
    ldr r1, =message
    ldr r2, =len
    mov r7, #4
    swi 0

    mov r7, #1
    swi 0

.data
message:
    .asciz "Hola mundo\n"
len = .-message

El equivalente del ejemplo anterior en código máquina (en hexadecimal y binario) sería algo así:

0x0100A0E3
0x10109FE5
0x10209FE5
0x0470A0E3
0x000000EF
0x0170A0E3
0x000000EF
0x24000000
0x0D000000
0000 0001 0000 0000 1010 0000 1110 0011
0001 0000 0001 0000 1001 1111 1110 0101
0001 0000 0010 0000 1001 1111 1110 0101
0000 0100 0111 0000 1010 0000 1110 0011
0000 0000 0000 0000 0000 0000 1110 1111
0000 0001 0111 0000 1010 0000 1110 0011
0000 0000 0000 0000 0000 0000 1110 1111
0010 0100 0000 0000 0000 0000 0000 0000
0000 1101 0000 0000 0000 0000 0000 0000

¿Qué es una ISA?

La hija de Raja Koduri, ex de AMD y actualmente empleado de Intel hizo un dibujo (basado en algunos dibujos que ya he visto antes en otros documentos usados en el ámbito educativo) que recupero aquí:

He querido comenzar con esa imagen puesto que creo que lo explica bastante bien. Una ISA (Instruction Set Architecture) o arquitectura del conjunto de instrucciones es el repertorio de instrucciones que puede entender y ejecutar la unidad de control de una determinada CPU. Eso no solo incluye las instrucciones (tipos, codificación,…) comprensibles por la CPU, sino también el tipo de datos aceptados de forma nativa, endianness, tamaño y cantidad de registros, arquitectura de la memoria, e interrupciones/excepciones, extensiones, branching, etc.

Es decir, sería la capa visible al programador. Por ello, sería muy interesante que comiences a relacionar esta serie de artículos con la serie de programación para entender cómo funcionan las computadoras.

Otra cosa muy diferente es cómo se puede implementar una ISA, es decir, cómo se puede crear una canalización, paralelismo, etc., es decir, cómo se van a procesar o tratar dichas instrucciones y datos. Esto es lo que se conoce como microarquitectura, y para implementar ésta se necesita una serie de unidades funcionales (multiplexores, ALU, FPU, registros, unidad de control, …), es decir, circuitería.

Por cierto, diferentes microarquitecturas pueden ser compatibles con una misma ISA, pero una microarquitectura no podría (de forma nativa) procesar otra ISA diferente para la que fue diseñada. Por ejemplo, la ISA AMD64 o x86-64 (Intel la llama EM64T) puede ser ejecutada por microarquitecturas tan diferentes como Intel Ice Lake o por AMD Zen 2, pero una microarquitectura Zen 2 no podría procesar una ISA SPARC, PPC, o ARM…

No confundas los términos ISA y microarquitectura (µarch) con arquitectura. La arquitectura de una computadora engloba tanto la ISA como la microarquitectura, además de otros aspectos más amplios del sistema en conjunto…

Tipos de ISA:

Broadwell vs Cortex A72 (tamaño)
Arriba una micrografía de un Intel Broadwell (he marcado con un recuadro azul la superficie de un solo núcleo). Cada núcleo del Broadwell tiene unas dimensiones de aprox. 8mm2   /    Abajo un núcleo ARM Cortex A72 de aprox. 1.15mm2

Dentro de las ISAs podemos encontrar varios tipos (vuelvo a repetir: tipos de ISA, no tipos de instrucciones que veremos más adelante), que principalmente se diferencian en el tamaño del repertorio de instrucciones y longitud:

  • CISC: son las siglas de Complex Instruction Set Computer, es decir, un modelo de ISA complejo. Se compone de instrucciones largas, gran número de ellas y que pueden realizar bastantes operaciones complejas con los oprandos situados en la memoria o registros. Aparecieron antes que otros tipos, por eso CISC es un retrónimo. Algunos ejemplos de CISC son IBM System/370, Zilog Z80, Motorola 68k, National Semiconductor 32016, MOS Technology 6502, DEC PDP-11, DEC VAX, x86 (pre-x86, x86-8, x86-16, x86-32, x86-64), IBM z/Architecture, etc.
  • RISC: son las siglas de Reduced Instruction Set Computer, es decir, un modelo de ISA simple que surge para resolver algunos problemas destacados de los CISC. A finales de los 70 se comenzó a ver un fenómeno curioso, y es que algunas versiones de arquitecturas CISC de gama baja a las que se le retiraban algunas de las instrucciones complejas para reducir costes en la implementación, en vez de ir peor mejoraban su rendimiento. Por ese motivo se comienzan a diseñar los primeros RISC con repertorios de instrucciones más reducidos e instrucciones simples. Ejemplos son el AMD 29000, ARM, DEC Alpha, RISC-V, MIPS, HP PA-RISC, IBM PowerPC/POWER, SPARC, etc.

Estos dos tipos tienen sus ventajas y desventajas destacadas:

RISC CISC
El tiempo de desarrollo para una microarquitectura de este tipo es más bajo y el coste más reducido. La complejidad hace que el coste y el tiempo invertido en la implementación sea mayor.
El tipo de unidad de control suele ser cableada, es decir, mucho más rápida y eficiente. Suele tener una unidad de control microprogramada, más lenta y menos eficiente.
Suelen tener gran cantidad de registros de propósito general. Dispone de pocos bancos de registros de propósito general.
El espacio de integración es más bajo en igualdad de condiciones. Es decir, el dado o core tendrá menor superficie. Suelen ocupar más espacio en el silicio, lo que encarece su producción y baja el yield.
El tamaño de instrucciones es fija y corta. En cuanto a cantidad, suele ser reducido, normalmente inferior a las 128 instrucciones. Suelen durar solo un ciclo de duración. Tamaño variable y largo. Gran cantidad de instrucciones, llegando a las 500 en algunos casos. Necesitan varios ciclos de duración, por tanto el CPI es mayor, o lo que es lo mismo, el IPC es menor.
La cantidad de modos de direccionamiento de memoria son pocos y simples. Tenemos más cantidad de modos de direccionamiento y complejos.
El tiempo de programación es más elevado. El tiempo de programación es menor, una de las pocas ventajas de CISC frente a RISC.
El compilador debe ser complejo y muy eficiente. El compilador no es crítico y puede ser más sencillo.
El tamaño del programa generado es hasta un 30% mayor al necesitar de más instrucciones simples para realizar una misma tarea en comparación con CISC. El programa suele ser más compacto, ya que con pocas instrucciones complejas basta.
La abstracción SW / HW es baja. El software está muy ligado al hardware. La abstracción es alta, siendo el software algo más independiente del hardware.

Estos son los dos grandes tipos, pero existen otros:

  • ZISC: Zero Instruction Set Computer no cuenta con un conjunto de instrucciones (en el sentido clásico). Se basa en la mera coincidencia de patrones, similar a los computadores analógicos donde tampoco existían, aunque aquí hablamos de computadoras digitales. Ha habido pocas implementaciones de este tipo, algún ejemplo sería el IBM ZISC35 o el ZISC78, e incluso algunos chips neuronales como el CM1K…
  • SISC: Specific Instruction Set Computer es similar a RISC, de hecho se podría catalogar como tal, pero es aún más reducidopara optimizar el rendimiento para alguna aplicación específica. Empleado en algunos microcontroladores y ASIPs. Un ejemplo, algunos autores dicen que el DSP TMS320 de TI (Texas Instruments) es SISC y otros lo etiquetan como RISC…
  • Otros: existen otros más raros y menos utilizados como:
    • VISC: Virtual Instruction Set Computer es otro tipo introducido por Soft Machines.
    • DISC: Dynamic Instruction Set Computer puede modificar el conjunto de instrucciones de forma dinámica según la demanda o exigencias del programa. Se suele usar para FPGAs reconfigurables.
    • NISC: No Instruction Set Computer tiene una tecnología muy específica de compilador y se emplea en CPUs de alta eficiencia para algunas aplicaciones críticas, aceleradores por hardware, etc. No necesita de ROM de microcódigo, ni controladores sofisticados, ni decodificador de instrucciones, etc.
    • MISC: Minimal Instruction Set Computer también es un conjunto muy reducido de instrucciones, pero a diferencia de otros como SISC o RISC, los operandos se almacenan en la pila en vez de en los registros, reduciendo así el tamaño de los operandos y simplificando al máximo la microarquitectura. Funciona rápido y la unidad de decodificación de instrucciones es más pequeña. Por lo general se usó en máquinas primitivas como EDSAC, Mark 1, ILLIAC, MANIAC I, etc.
    • EDGE: Explicit Data Graph Execution intenta mejorar las carencias de CISC y evitar los problemas de cuello de botella. Contiene muchas instrucciones individuales en un grupo denominado hyperblock y puede ser ejecutado en paralelo.
    • OISC: One Instruction Set Computer, también llamado URISC (Ultimate RISC). Usa solo una instruccion sin limitar las aplicaciones. Sus implementaciones se usan especialmente para la enseñanza. Imagina una instrucción SUBLEQ que reste el contenido de dos direcciones de memoria y almacene el resultado en otra dirección. Con esa única instrucción, ejecutandola secuencialmente se podrían conseguir los equivalentes a varias instrucciones. Por ejemplo, si se ejecuta SUBLEQ b, b + SUBLEQ a, Z + SUBLEQ Z, b + SUBLEQ Z, Z = MOV a, b.

¿Se puede implementar una ISA de un tipo concreto desde una microarquitectura que opera de forma diferente? Aunque parezca raro, la respuesta es sí. Y esto me sirve de nexo para explicar otros dos tipos muy interesantes:

Híbridos CISC-RISC (RISC-Like):

DieShot AMD K5 PR75
Die Shot: AMD K5 PR75

Algunos diseñadores de microarquitecturas CISC han querido obtener las ventajas de RISC sin abandonar el modelo CISC o sin perjudicar a la compatibilidad del software escrito para su ISA. Eso sería el caso de x86. CISC es un modelo antiguo y con carencias, pero ¿cómo pasarse a RISC sin crear una nueva ISA y sin que todo el software compatible tenga que ser compilado para esta nueva arqutiectura? La solución es la hibridación.

A Intel y AMD cada vez se les hacía más cuesta arriba mejorar sus microarquitecturas para extraer más rendimiento y, aún peor, crear sistemas superescalares era muy complejo en la CISC x86. De hecho, pocos modelos consiguieron ser superescalares a excepción del Intel Pentium y el Cyrix 6×86. El Pentium lo tuvo más fácil, puesto que era ejecución en orden, y Cyrix era algo más complejo ya que era ejecución fuera de orden.

Yale Patt de la Universidad de Texas trajo la salvación con su HPS (High Performance Substrate). Podía hacer algo hasta la fecha impensable, traducir instrucciones CISC en múltiples RISC denominadas microoperaciones. Eso se consigue haciendo pasar las instrucciones CISC traídas desde la RAM a la cache L1 de instrucciones, pasarlas por la unidad fetch y luego, cuando llegan al decodificador, se traducen en varias microoperaciones que se almacenan en una cola para su ejecución.

A partir de ese momento (Front-End de la CPU), el microprocesador opera como un RISC (todo el Back-End), simplificando la circuitería y posibilitando que se puedan implementar tecnologías superescalares, fuera de orden, etc., que si siguiesen trabajando como CISC sería demasiado complejo y costoso.

Uno de los primeros diseños RISC-like sería el AMD K5, que usaba la exitosa microarquitectura del 29k a la que le añadieron un Front-End para traducir las CISC-x86 y posibilitando que el Back-End trabajase de forma superescalar, con ejecución fuera de orden, ejecución especulativa, renombre de registros, etc., sin alterar la compatibilidad con x86.

Actualmente, los diseños de Intel y AMD trabajan como RISC a nivel electrónico, aunque el repertorio de instrucciones sea CISC…

VLIW (Very Long Instruction Word):

Diagrama superescalar vs VLIW

Muchos autores incluyen VLIW como un tipo de ILP, y tienen razón en parte, pero a mi me gusta incluirlo dentro de los tipos de ISA. ¿Mi motivo? VLIW necesita de una determinada densidad de instrucciones con una longitud concreta, por eso yo lo entiendo como un tipo de ISA. Aunque, si lo piensas en que se pueden «camuflar» repertorios RISC o CISC dentro de VLIW, como ocurre con los RISC-like.

En este caso, la longitud de las instrucciones es bastante largo, de 128 a 1024 bits, pero no existen gran cantidad de ellas. Es decir, se parece un poco a CISC y también a RISC. El objetivo es que este tipo de instrucciones se encajen de forma precisa en cada unidad funcional disponible en la CPU, simplificando el hardware al dejar la planificación del código en manos del compilador (o programador si hablamos de ensamblador). VLIW se parece bastante a la arquitectura superescalar tradicional, pero los diferencia de ellos en ese detalle de la planificación que he comentado.

En un VLIW se puede empaquetar varias instrucciones en una larga para que encajen perfectamente con las unidades funcionales disponibles. Por ejemplo, imagina que tienes una CPU con una unidad de ejecución para suma, otra para resta y otra para multiplicación. Podrías generar una instrucción larga que agrupe una instrucción de suma, otra de resta y otra de multiplicación que se ejecutarán de una sola vez.

Eso reduce, no solo la complejidad de hardware, también el consumo. No obstante, no todo son ventajas, ya que eso genera una incompatibilidad binaria si se quiere cambiar el número o tipo de unidades funcionales disponible. Aunque sea compatible con una misma ISA las instrucciones VLIW serían diferentes para adaptarse al nuevo ancho de ejecución y por tanto, los binarios deben ser compilados de nuevo.

También habría algunos conflictos adicionales, necesita de un gran ancho de banda y de bus, compiladores muy complejos, la decodificación tienen un coste adicional debido a la complejidad de las instrucciones, las unidades deben ser de tipo lockstep, etc. Y lo más importante, no siempre se pueden empaquetar instrucciones para mantener ocupadas a todas las unidades funcionales disponibles, lo que hace que permanezcan en estado ocioso en muchos momentos.

Finalizo agregando que ha habido algunos diseños interesantes de tipo VLIW a lo largo de la historia. Por ejemplo, dentro de la isa HP PA-RISC se comenzó a coquetear con este tipo de implementación que luego serían transladados a la IA-64 para el Itanium de Intel (véase EPIC o Explicitly Parallel Instruction Computing).

Pero uno de los diseños más destacados, y ahora entenderás por qué he incluído este apartado aquí, es el de la compañía Transmeta para sus Crusoe y Efficeon. Ambos eran compatibles con la CISC x86, y para ello se debía desacoplar las instrucciones del hardware. Linus Torvalds, que comenzó a trabajar en esta compañía en aquella época, se encargó de escribir un software llamado Code Morphing.

Con Code Morphing se podían traducir los códigos x86 a instrucciones largas de 64 y 128 bits para la CPU VLIW de Transmeta. Es decir, implementan capa de software adicional que necesita estar siempre en ejecución para la traducción en tiempo real de las instrucciones, evitando así los complicados Front-End de los RISC-likes. El resultado fue un chip extremadamente sencillo que conseguía un consumo eléctrico ridículamente bajo frente a los modelos de Intel y AMD de la época.

¿Problemas? Que Code Morphing debía estar alojado en una ROM y ser el primer programa que se ejecuta al iniciar el sistema y siempre está latente para aislar las instrucciones de los programas x86 para que puedan ser acopladas a VLIW y eso consume ciclos de reloj adicionales y otros recursos que no se están destinando para las aplicaciones…

¡Hasta la siguiente parte!

Isaac

Apasionado de la computación y la tecnología en general. Siempre intentando desaprender para apreHender.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

A %d blogueros les gusta esto:

Si continuas utilizando este sitio aceptas el uso de cookies. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar