Introducción
Hola a todos. Bienvenidos a noviembre. Espero que estén muy bien.
Tuvimos un octubre súper ocupado. Acabamos de lanzar una gran mejora a las soluciones de la comunidad. Ahora las deduplicamos, así que las soluciones similares solo aparecen una vez; agregamos nuevas opciones de ordenamiento, la posibilidad de buscar por código y, si revisas C#, verás que también agregamos la opción de filtrar por distintos conceptos de programación. Así puedes buscar soluciones que usen desplazamiento de bits, recursión o lo que se te antoje. Vamos a ir implementando eso en otras tracks durante la semana de la comunidad.
Pero por ahora, concentrémonos en #12in23. Octubre fue un mes interesante: exploramos los lenguajes orientados a objetos, pero este mes vamos a fondo. Nos vamos a enfocar en los lenguajes ensamblador y, en concreto, en el ensamblador MIPS, el ensamblador x86-64 y WebAssembly. Como siempre, Erik nos va a contar qué hace que estos lenguajes sean interesantes y únicos.
Las insignias
Como siempre, puedes ganar la insignia Nibbly November completando 5 ejercicios cualesquiera en estos lenguajes. También tenemos la insignia Year-Long, que sé que muchos de ustedes quieren conseguir. Para eso tenemos 5 ejercicios destacados que puedes completar. Son estos:
- Conteo de bits: cuenta los bits iguales a 1 en un número
- Granos: calcula la cantidad de granos en un tablero de ajedrez donde cada casilla duplica la anterior
- Color de la resistencia: convierte el color de una banda de resistencia en su representación numérica
- Cifrado rotacional: implementa el cifrado rotacional (también conocido como cifrado César)
- Conteo de nucleótidos: calcula cuántas veces aparece cada nucleótido en una cadena de ADN
Antecedentes
¿Por qué se llama Nibble November?
Bueno, como probablemente sabes, un byte son 8 bits. Y un nibble son 4 bits. El nombre empieza a tener sentido si cambias la y de byte por una i, con lo que obtienes bite. Un nibble es, entonces, un pequeño bocado.
¿Qué es un lenguaje ensamblador?
Bueno, empecemos por lo básico. Una CPU ejecuta instrucciones, como «sumar dos números» o «desplazar bits a la izquierda», etc. Estas instrucciones se conocen como instrucciones de código máquina y no son más que secuencias específicas de bits. Ejecutar un programa es, «simplemente» (entre comillas), el proceso por el que la CPU procesa y ejecuta esas secuencias de bits.
Como escribir las instrucciones directamente como secuencias de bits es engorroso y propenso a errores, Kathleen y Andrew Donald Booth idearon un lenguaje más amigable para las personas con el fin de representar las instrucciones de código máquina, nada menos que en 1947. Un lenguaje así, que representa instrucciones de código máquina, se llama lenguaje ensamblador. Ese lenguaje ensamblador se convierte luego en instrucciones de código máquina mediante un «ensamblador».
Un aspecto interesante de los lenguajes ensamblador es que están directamente ligados a una arquitectura de CPU, aunque por lo general son independientes del sistema operativo.
Por cierto, si recuerdas las tarjetas perforadas, esos grandes trozos de cartón que se usaban en las primeras computadoras para hacerlas ejecutar programas, ¡también eran un lenguaje ensamblador!
¿En qué se diferencia el lenguaje ensamblador de los lenguajes con los que programamos hoy?
La diferencia clave es que los lenguajes ensamblador son de muy bajo nivel. Vas a echar de menos muchas de las abstracciones a las que te habías acostumbrado. Las clases y los objetos no existen por ningún lado. ¿Bucles? Tendrás que escribirlos a mano con saltos. ¿Funciones? ¡Para nada! Escribir código ensamblador fue toda una lección de humildad, porque ahí te das cuenta de cuánto te facilitan la vida los lenguajes modernos. Pero también es increíblemente útil escribir código ensamblador, porque vas a entender mucho mejor cómo funcionan las cosas en realidad.
Dato curioso: ¡el 99 % del código fuente de RollerCoaster Tycoon era código ensamblador escrito a mano! Es un logro asombroso, que vas a apreciar aún más una vez que hayas escrito un poco de ensamblador por tu cuenta.
¿Todavía hay gente que escribe código ensamblador?
Bueno, menos que antes. Antes solía pasar que el ensamblador escrito a mano superaba en rendimiento al código máquina generado por el compilador (una de las razones por las que C++ permite incorporar código ensamblador directamente), pero los compiladores se han vuelto tan buenos generando código máquina que eso ya casi nunca es cierto. Dicho eso, todavía vas a encontrar lenguaje ensamblador en entornos críticos para el rendimiento o con recursos limitados.
Resúmenes
MIPS
- MIPS (Microprocessor without Interlocked Pipelined Stages) es una familia de arquitecturas de conjunto de instrucciones de tipo computadora con conjunto reducido de instrucciones (RISC)
- Desarrollada por MIPS Computer Systems y publicada por primera vez en 1985
- Varias versiones: MIPS I, II, III, IV, V y MIPS32/64. Las dos primeras versiones eran solo de 32 bits, pero MIPS III introdujo el soporte de 64 bits.
- Varias extensiones opcionales, como instrucciones SIMD y compresión
- Muy influyente en las arquitecturas RISC posteriores
- En 2021, MIPS anunció que la arquitectura MIPS ya no se seguiría desarrollando y que habían migrado a RISC-V (una arquitectura de código abierto y libre de regalías)
- Se usa sobre todo en sistemas embebidos (por ejemplo, routers) y servidores (las computadoras Silicon Graphics lo usaban y fueron famosas por los efectos especiales de películas), el supercomputadora NEC Cenju-4, el auto Model S de Tesla, la sonda New Horizons de la NASA, y también para enseñar ensamblador en universidades y en varias consolas de videojuegos (por ejemplo, la PlayStation original, la PlayStation Portable y la Nintendo 64)
Ensamblador x86-64
- Diseñado por AMD y publicado en 1999 como la arquitectura AMD64
- Es una versión de 64 bits del conjunto de instrucciones x86, que se remonta a 1978, cuando Intel lanzó su microprocesador 8086. Era un procesador de 16 bits, pero más adelante el 80386 añadió instrucciones de 32 bits, y ese conjunto de instrucciones se volvió sinónimo de x86.
- Lo principal que habilitaron los 64 bits fue la posibilidad de direccionar más memoria (el direccionamiento de 32 bits está limitado a 4 GB), que se había convertido en un cuello de botella. En teoría, los 64 bits pueden direccionar 16 exabytes, pero actualmente solo se usan 48 bits, lo que permite direccionar 256 TB (se puede ampliar más adelante cuando haga falta)
- AMD64 extiende el conjunto de instrucciones x86 y se diseñó para ser totalmente compatible con las aplicaciones existentes de 16 y 32 bits mediante un modo de compatibilidad
- Intel había diseñado IA-64 sin la participación de AMD. Era un conjunto de instrucciones de 64 bits nuevo, muy distinto y sin compatibilidad con versiones anteriores. Al final, AMD64 se impuso e Intel implementó su propia versión (con apenas diferencias semánticas menores)
- Se usa en todas partes. Desde estaciones de trabajo hasta servidores (incluidas las supercomputadoras), desde sistemas embebidos hasta consolas de videojuegos (por ejemplo, la PS5 y la Xbox Series X).
WebAssembly
- Diseñado por el W3C, un organismo de estandarización para las tecnologías web
- Sus objetivos de diseño son:
- Rápido, seguro y portátil
- Una representación eficiente y portátil
- En aquellos tiempos, la ejecución rápida en la web se lograba normalmente con plugins especializados del navegador, como Flash y Silverlight, ya que JavaScript por sí solo no se presta muy bien a la computación de alto rendimiento. Los principales inconvenientes de esos plugins eran que solían tener muchos problemas de seguridad y no estaban estandarizados.
- Mozilla diseñó asm.js, un subconjunto de JavaScript que buscaba permitir ejecutar código en el navegador con muy buen rendimiento, lo que lograba mediante la consistencia de tipos (sin cambios dinámicos de tipo) y sin recolección de basura. Los lenguajes podían entonces compilar a asm.js y aun así obtener buen rendimiento al ejecutarse en la web. Pero seguía siendo JS, así que estaba limitado en lo que se podía lograr. De ahí la propuesta de un nuevo lenguaje: WASM.
- Es un lenguaje similar al ensamblador, en el sentido de que ofrece un conjunto de instrucciones para ejecutar. Algo clave es que no está atado a una CPU específica, así que es independiente de la plataforma y requiere una implementación para cada plataforma (una máquina virtual). Esto significa que WebAssembly es, de hecho, bytecode, no código máquina
- Tiene tipado estático (una diferencia clave con JS)
- Normalmente usa compilación anticipada o compilación justo a tiempo (pero puede interpretarse)
- Es un estándar abierto y define dos cosas:
- Un formato binario
- Un formato de texto (que se compila al formato binario)
- Hay implementaciones en todos los navegadores principales
- Se usa en muchas páginas web que requieren alto rendimiento, como Google Earth, Figma, Unity y Autocad. También ha ido ganando terreno en el lado del servidor, por ejemplo para ejecutar microservicios, correr en plataformas SaaS (por ejemplo, CloudFlare workers) o en Docker
Y desde el punto de vista de la programación, ¿en qué se diferencian?
MIPS
- Usa una arquitectura de carga-almacenamiento (también conocida como registro-registro), en la que las instrucciones o bien acceden a memoria o bien hacen aritmética, pero siempre operan exclusivamente con datos que están en registros
Ensamblador x86-64
- Usa una arquitectura registro-memoria, que permite realizar operaciones sobre la memoria (o desde ella), además de sobre los registros
WebAssembly
- Usa programación basada en pila (sin registros) con la opción de leer datos desde la memoria y escribirlos en ella
¿Qué hace que estos lenguajes sean geniales?
MIPS
- Eficiencia. Los procesadores MIPS son muy eficientes, lo que los hace ideales para sistemas embebidos.
- Rendimiento. Tienen un gran rendimiento, de ahí que MIPS se haya usado en supercomputadoras.
- Fáciles de aprender. Con pocas instrucciones y cada una haciendo solo una cosa sencilla, es fácil de aprender. Ideal con fines didácticos.
Ensamblador x86-64
- Potente. x86-64 se ha ido ajustando durante décadas, con muchísimas instrucciones que ayudan con el rendimiento. Un ejemplo es SIMD (Single Instruction, Multiple Data), instrucciones que, bueno, permiten que una sola instrucción se ejecute en paralelo sobre varios datos.
- Omnipresente. Los dispositivos que ejecutan x86-64 están por todas partes. Las CPU de Intel y AMD lo implementan. Ha sido el estándar de facto durante bastante tiempo.
- Familiar. Como extiende el conjunto de instrucciones x86, será relativamente fácil de aprender para quienes ya conocen ese conjunto de instrucciones Documentación amplia y detallada
- Estable. Aunque se añaden versiones nuevas con regularidad, el núcleo sigue siendo extremadamente estable y compatible con versiones anteriores
WebAssembly
- La máquina virtual de WebAssembly, basada en pila, es ligera y sencilla en comparación con los lenguajes ensamblador de los procesadores físicos (incluido RISC). Eso hace que sea relativamente fácil compilar para ella.
- El formato de texto de WebAssembly usa «azúcar» de S-Expression para lograr un estilo imperativo conocido, que se transforma en código basado en pila. La forma con S-Expressions se llama «forma azucarada» y se «desazucara» a la otra forma, equivalente a lo que hay en el binario. Las S-expressions te resultarán familiares si alguna vez has trabajado con un LISP
- Buena interoperabilidad con JavaScript. Es sencillo pasar datos desde y hacia JavaScript. Una advertencia importante: WASM todavía no permite interactuar con el DOM
- Se mejora continuamente. No solo se mejoran las máquinas virtuales de WASM, sino que el propio estándar también está en desarrollo activo. Se están diseñando y trabajando muchísimas funcionalidades nuevas, como instrucciones relacionadas con SIMD, recolección de basura, hilos, optimizaciones de llamadas de cola y más
- Seguro. El código se valida y se ejecuta en un entorno aislado, lo que ofrece un mayor grado de validación estática en comparación con JavaScript o los lenguajes ensamblador nativos. La semántica está bien definida, lo que las hace fáciles de comprobar y de razonar.
Características destacadas
MIPS
- Eficiencia. Los procesadores MIPS son muy eficientes, lo que los hace ideales para sistemas embebidos.
- Rendimiento. Tienen un gran rendimiento, de ahí que MIPS se haya usado en supercomputadoras.
- Fáciles de aprender. Con pocas instrucciones y cada una haciendo solo una cosa sencilla, es fácil de aprender. Ideal con fines didácticos.
Ensamblador x86-64
- Potente. x86-64 se ha ido ajustando durante décadas, con muchísimas instrucciones que ayudan con el rendimiento. Un ejemplo es SIMD (Single Instruction, Multiple Data), instrucciones que, bueno, permiten que una sola instrucción se ejecute en paralelo sobre varios datos.
- Omnipresente. Los dispositivos que ejecutan x86-64 están por todas partes. Las CPU de Intel y AMD lo implementan. Ha sido el estándar de facto durante bastante tiempo.
- Se actualiza con regularidad. Por ejemplo, nuevas instrucciones vectoriales, con SSE3-5, AVX, AVX-512 y más.
WebAssembly
- Eficiente. El formato binario es compacto y se puede decodificar, validar y compilar en una sola pasada rápida. También admite streaming, lo que permite que la decodificación, la validación y la compilación empiecen lo antes posible, antes de haber visto todos los datos. Y además es paralelizable. Esto lo hace perfecto para aplicaciones web de alto rendimiento.
- Un gran objetivo de compilación. Permite que el código de muchos lenguajes se ejecute en la web. La mayoría de los lenguajes importantes admiten compilar a binarios de WebAssembly, lo que significa que tu código puede ejecutarse en la web sin necesidad de escribir JavaScript. Algunos lenguajes no compilan tu código a WebAssembly, sino que compilan el entorno de ejecución a WebAssembly, que luego puede ejecutar bytecode sin modificaciones.
- Fácil de desplegar y ejecutar. Todo lo que necesitas es una máquina virtual que pueda interpretar el bytecode, y todos los navegadores principales incluyen una.
- No está atado a la web, sino que también puede ejecutarse en el lado del servidor. La interfaz del sistema de WebAssembly (WASI) es una interfaz (ABI y API) diseñada para ser portátil a cualquier plataforma. Es similar a POSIX (interfaces estándar para sistemas Unix) y ofrece cosas como E/S. La seguridad es una parte clave de su diseño, que incluye el aislamiento y estar orientada a capacidades (tienes que pedir explícitamente permisos para cosas como archivos o sockets). WASI también tiene el potencial de permitir una fácil interconexión entre lenguajes. Solomon Hykes, cofundador de Docker, escribió en 2019: «Si WASM+WASI hubiera existido en 2008, no habríamos necesitado crear Docker»
Cuál elegir
- Si nunca has trabajado con un lenguaje ensamblador, WebAssembly es probablemente el más fácil para empezar. Dicho eso, si quieres aprender un lenguaje ensamblador que compile a código máquina, prueba con el ensamblador MIPS
- Si trabajas en una máquina x86-64 (muy probable), prueba el ensamblador x86-64
- Si conoces LISP, vas a apreciar que WebAssembly use S-expressions
- Si trabajas con aplicaciones web, WebAssembly es la opción más lógica
- Si te importa el rendimiento, x86-64 y MIPS son excelentes opciones. O, si te importa el rendimiento web, prueba WebAssembly