¿Qué es LLVM? El poder detrás de Swift, Rust, Clang y más

Los nuevos lenguajes y las mejoras de los existentes se están multiplicando por todo el panorama del desarrollo. Mozilla's Rust, Apple's Swift, Jetbrains's Kotlin y muchos otros lenguajes brindan a los desarrolladores una nueva gama de opciones de velocidad, seguridad, conveniencia, portabilidad y potencia.

¿Porqué ahora? Una gran razón son las nuevas herramientas para crear lenguajes, específicamente, compiladores. Y el principal de ellos es LLVM, un proyecto de código abierto desarrollado originalmente por el creador del lenguaje Swift Chris Lattner como un proyecto de investigación en la Universidad de Illinois.

LLVM facilita no solo la creación de nuevos lenguajes, sino también la mejora del desarrollo de los existentes. Proporciona herramientas para automatizar muchas de las partes más ingratas de la tarea de creación de un lenguaje: crear un compilador, portar el código generado a múltiples plataformas y arquitecturas, generar optimizaciones específicas de la arquitectura como la vectorización y escribir código para manejar metáforas de lenguaje común como excepciones. Su licencia liberal significa que se puede reutilizar libremente como un componente de software o implementar como un servicio.

La lista de idiomas que utilizan LLVM tiene muchos nombres familiares. El lenguaje Swift de Apple usa LLVM como su marco de compilación, y Rust usa LLVM como un componente central de su cadena de herramientas. Además, muchos compiladores tienen una edición LLVM, como Clang, el compilador C / C ++ (este es el nombre, "C-lang"), un proyecto en sí mismo estrechamente aliado con LLVM. Mono, la implementación de .NET, tiene una opción para compilar en código nativo usando un back-end LLVM. Y Kotlin, nominalmente un lenguaje JVM, está desarrollando una versión del lenguaje llamado Kotlin Native que usa LLVM para compilar en código nativo de la máquina.

LLVM definido

En esencia, LLVM es una biblioteca para crear código nativo de máquina mediante programación. Un desarrollador usa la API para generar instrucciones en un formato llamado representación intermedia o IR. Luego, LLVM puede compilar el IR en un binario independiente o realizar una compilación JIT (justo a tiempo) en el código para ejecutarlo en el contexto de otro programa, como un intérprete o tiempo de ejecución para el lenguaje.

Las API de LLVM proporcionan primitivas para desarrollar muchas estructuras y patrones comunes que se encuentran en los lenguajes de programación. Por ejemplo, casi todos los lenguajes tienen el concepto de función y de variable global, y muchos tienen corrutinas e interfaces C de funciones foráneas. LLVM tiene funciones y variables globales como elementos estándar en su IR, y tiene metáforas para crear corrutinas e interactuar con bibliotecas C.

En lugar de dedicar tiempo y energía a reinventar esas ruedas en particular, puede usar las implementaciones de LLVM y concentrarse en las partes de su lenguaje que necesitan atención.

Leer más sobre Go, Kotlin, Python y Rust 

Vamos:

  • Aprovecha el poder del idioma Go de Google
  • Los mejores IDE y editores de Go language

Kotlin:

  • ¿Qué es Kotlin? La alternativa de Java explicada
  • Framework Kotlin: una encuesta de herramientas de desarrollo de JVM

Pitón:

  • ¿Qué es Python? Todo lo que necesitas saber
  • Tutorial: cómo empezar con Python
  • 6 bibliotecas esenciales para todo desarrollador de Python

Oxido:

  • ¿Qué es el óxido? La forma de realizar un desarrollo de software seguro, rápido y sencillo
  • Aprenda a empezar con Rust 

LLVM: diseñado para la portabilidad

Para entender LLVM, podría ser útil considerar una analogía con el lenguaje de programación C: C a veces se describe como un lenguaje ensamblador portátil de alto nivel, porque tiene construcciones que pueden correlacionarse estrechamente con el hardware del sistema y se ha portado a casi cada arquitectura del sistema. Pero C es útil como lenguaje ensamblador portátil solo hasta cierto punto; no fue diseñado para ese propósito en particular.

Por el contrario, el IR de LLVM se diseñó desde el principio para ser un conjunto portátil. Una forma de lograr esta portabilidad es ofreciendo primitivas independientes de cualquier arquitectura de máquina en particular. Por ejemplo, los tipos de números enteros no se limitan al ancho de bits máximo del hardware subyacente (como 32 o 64 bits). Puede crear tipos de enteros primitivos utilizando tantos bits como necesite, como un entero de 128 bits. Tampoco tiene que preocuparse por crear la salida para que coincida con el conjunto de instrucciones de un procesador específico; LLVM también se ocupa de eso.

El diseño de arquitectura neutral de LLVM facilita la compatibilidad con hardware de todo tipo, presente y futuro. Por ejemplo, IBM contribuyó recientemente con código para dar soporte a su z / OS, Linux on Power (incluido el soporte para la biblioteca de vectorización MASS de IBM) y arquitecturas AIX para proyectos LLVM C, C ++ y Fortran. 

Si desea ver ejemplos en vivo de LLVM IR, vaya al sitio web del Proyecto ELLCC y pruebe la demostración en vivo que convierte el código C en LLVM IR directamente en el navegador.

Cómo los lenguajes de programación usan LLVM

El caso de uso más común para LLVM es como un compilador anticipado (AOT) para un lenguaje. Por ejemplo, el proyecto Clang compila de antemano C y C ++ en binarios nativos. Pero LLVM también hace posibles otras cosas.

Compilación justo a tiempo con LLVM

Algunas situaciones requieren que se genere código sobre la marcha en tiempo de ejecución, en lugar de compilarlo con anticipación. El lenguaje Julia, por ejemplo, JIT-compila su código, porque necesita ejecutarse rápidamente e interactuar con el usuario a través de un REPL (bucle de lectura-evaluación-impresión) o un indicador interactivo. 

Numba, un paquete de aceleración matemática para Python, compila con JIT funciones seleccionadas de Python en código de máquina. También puede compilar código decorado con Numba antes de tiempo, pero (como Julia) Python ofrece un desarrollo rápido al ser un lenguaje interpretado. El uso de la compilación JIT para producir dicho código complementa mejor el flujo de trabajo interactivo de Python que la compilación anticipada.

Otros están experimentando con nuevas formas de usar LLVM como JIT, como compilar consultas PostgreSQL, lo que produce un aumento de hasta cinco veces en el rendimiento.

Optimización automática de código con LLVM

LLVM no solo compila el IR en código de máquina nativo. También puede dirigirlo mediante programación para optimizar el código con un alto grado de granularidad, durante todo el proceso de vinculación. Las optimizaciones pueden ser bastante agresivas, incluidas cosas como funciones en línea, eliminación de código muerto (incluidas declaraciones de tipos y argumentos de funciones no utilizados) y desenrollar bucles.

Nuevamente, el poder está en no tener que implementar todo esto usted mismo. LLVM puede manejarlos por usted, o puede indicarle que los desactive según sea necesario. Por ejemplo, si desea binarios más pequeños a costa de un poco de rendimiento, puede hacer que la interfaz del compilador le indique a LLVM que desactive el desenrollado de bucles.

Idiomas específicos de dominio con LLVM

LLVM se ha utilizado para producir compiladores para muchos lenguajes de propósito general, pero también es útil para producir lenguajes que son muy verticales o exclusivos de un dominio de problemas. De alguna manera, aquí es donde LLVM brilla más, porque elimina gran parte de la monotonía en la creación de dicho lenguaje y hace que funcione bien.

El proyecto Emscripten, por ejemplo, toma el código LLVM IR y lo convierte en JavaScript, lo que en teoría permite que cualquier lenguaje con un back-end LLVM exporte código que se pueda ejecutar en el navegador. El plan a largo plazo es tener backends basados ​​en LLVM que puedan producir WebAssembly, pero Emscripten es un buen ejemplo de lo flexible que puede ser LLVM.

Otra forma en que se puede utilizar LLVM es agregar extensiones específicas de dominio a un idioma existente. Nvidia usó LLVM para crear el compilador Nvidia CUDA, que permite que los lenguajes agreguen soporte nativo para CUDA que se compila como parte del código nativo que está generando (más rápido), en lugar de ser invocado a través de una biblioteca enviada con él (más lento).

El éxito de LLVM con lenguajes de dominios específicos ha impulsado nuevos proyectos dentro de LLVM para abordar los problemas que crean. El mayor problema es cómo algunas DSL son difíciles de traducir a LLVM IR sin mucho trabajo duro en la interfaz. Una solución en proceso es el proyecto de Representación intermedia de niveles múltiples o MLIR.

MLIR proporciona formas convenientes de representar operaciones y estructuras de datos complejas, que luego se pueden traducir automáticamente a LLVM IR. Por ejemplo, el marco de trabajo de aprendizaje automático de TensorFlow podría tener muchas de sus operaciones complejas de gráficos de flujo de datos compiladas de manera eficiente en código nativo con MLIR.

Trabajar con LLVM en varios idiomas

La forma típica de trabajar con LLVM es a través de código en un lenguaje con el que se sienta cómodo (y que tenga soporte para las bibliotecas de LLVM, por supuesto).

Dos opciones de lenguaje comunes son C y C ++. Muchos desarrolladores de LLVM utilizan uno de esos dos de forma predeterminada por varias buenas razones: 

  • LLVM en sí está escrito en C ++.
  • Las API de LLVM están disponibles en encarnaciones C y C ++.
  • Gran parte del desarrollo del lenguaje tiende a ocurrir con C / C ++ como base.

Aún así, esos dos idiomas no son las únicas opciones. Muchos lenguajes pueden llamar de forma nativa a bibliotecas C, por lo que, en teoría, es posible realizar el desarrollo LLVM con cualquiera de estos lenguajes. Pero ayuda tener una biblioteca real en el lenguaje que envuelve elegantemente las API de LLVM. Afortunadamente, muchos lenguajes y tiempos de ejecución de lenguajes tienen tales bibliotecas, incluidas C # / .NET / Mono, Rust, Haskell, OCAML, Node.js, Go y Python.

Una advertencia es que algunos de los enlaces de lenguaje a LLVM pueden ser menos completos que otros. Con Python, por ejemplo, hay muchas opciones, pero cada una varía en su integridad y utilidad:

  • llvmlite, desarrollado por el equipo que crea Numba, se ha convertido en el candidato actual para trabajar con LLVM en Python. Implementa solo un subconjunto de la funcionalidad de LLVM, según lo dictado por las necesidades del proyecto Numba. Pero ese subconjunto proporciona la gran mayoría de lo que necesitan los usuarios de LLVM. (llvmlite es generalmente la mejor opción para trabajar con LLVM en Python).
  • El proyecto LLVM mantiene su propio conjunto de enlaces a la API C de LLVM, pero actualmente no se mantienen.
  • llvmpy, el primer enlace popular de Python para LLVM, dejó de recibir mantenimiento en 2015. Malo para cualquier proyecto de software, pero peor cuando se trabaja con LLVM, dada la cantidad de cambios que se producen en cada edición de LLVM.
  • llvmcpy tiene como objetivo actualizar los enlaces de Python para la biblioteca C, mantenerlos actualizados de forma automatizada y hacerlos accesibles utilizando los modismos nativos de Python. llvmcpy se encuentra todavía en las primeras etapas, pero ya puede realizar un trabajo rudimentario con las API de LLVM.

Si tiene curiosidad sobre cómo usar las bibliotecas LLVM para construir un lenguaje, los propios creadores de LLVM tienen un tutorial, usando C ++ u OCAML, que lo guiará en la creación de un lenguaje simple llamado Kaleidoscope. Desde entonces ha sido portado a otros idiomas:

  • Haskell:  un puerto directo del tutorial original.
  • Python: uno de estos puertos sigue de cerca el tutorial, mientras que el otro es una reescritura más ambiciosa con una línea de comandos interactiva. Ambos usan llvmlite como enlaces a LLVM.
  • Rust Swift: Parecía inevitable que obtuviéramos puertos del tutorial a dos de los lenguajes que LLVM ayudó a crear.

Finalmente, el tutorial también está disponible en  lenguajes humanos . Ha sido traducido al chino, usando C ++ y Python originales.

Lo que LLVM no hace

Con todo lo que ofrece LLVM, es útil saber también lo que no hace.

Por ejemplo, LLVM no analiza la gramática de un idioma. Muchas herramientas ya hacen ese trabajo, como lex / yacc, flex / bison, Lark y ANTLR. El análisis está destinado a desacoplarse de la compilación de todos modos, por lo que no es sorprendente que LLVM no intente abordar nada de esto.

LLVM tampoco aborda directamente la cultura más amplia del software en torno a un idioma determinado. Instalar los binarios del compilador, administrar los paquetes en una instalación y actualizar la cadena de herramientas, debe hacerlo usted mismo.

Finalmente, y lo más importante, todavía hay partes comunes de los lenguajes para las que LLVM no proporciona primitivas. Muchos lenguajes tienen alguna forma de administración de memoria recolectada de basura, ya sea como la forma principal de administrar la memoria o como complemento de estrategias como RAII (que utilizan C ++ y Rust). LLVM no le proporciona un mecanismo de recolección de basura, pero proporciona herramientas para implementar la recolección de basura al permitir que el código se marque con metadatos que facilitan la escritura de recolectores de basura.

Sin embargo, nada de esto descarta la posibilidad de que LLVM eventualmente agregue mecanismos nativos para implementar la recolección de basura. LLVM se está desarrollando rápidamente, con un lanzamiento importante cada seis meses aproximadamente. Y es probable que el ritmo de desarrollo solo se acelere gracias a la forma en que muchos lenguajes actuales han puesto LLVM en el centro de su proceso de desarrollo.