Edward Kmett lleva Haskell a la JVM con THC, un JIT sobre Truffle y GraalVM
El Turbo Haskell compiler implementa los prim-ops de GHC 9.14.1 y ejecuta el Core que produce GHC dentro de la JVM. Tambien compila AOT con Native Image y ofrece FFI hacia Python, Ruby, R y JavaScript.
Edward Kmett empezo a escribir THC hace una semana como una broma, mientras estaba de vacaciones visitando a Bartosz Milewski. Hoy ese "Turbo Haskell compiler" implementa todos los prim-ops de GHC 9.14.1 y ofrece un JIT para GHC Core que ejecuta Haskell sobre la JVM. Por debajo usa Truffle y GraalVM, la misma tecnica que Kmett ya desarrollo en Cadenza.
El reparto con GHC
La division de trabajo es clara. GHC sigue encargandose del parseo, el chequeo de tipos, el desugaring y la optimizacion de Core. THC recoge ese Core y lo compila y ejecuta en su propio runtime. Con eso cubre caracteristicas avanzadas como Template Haskell y Linear Haskell. Y no solo funciona como JIT para Haskell de nivel GHC: tambien compila por adelantado (AOT) con Native Image, de modo que produce ejecutables.
Entre los programas que ya compila, en JIT o en AOT, estan pandoc, happy, alex y, desde hoy, el propio GHC. Resuelve paquetes con Cabal y soporta paquetes con varias librerias, Backpack incluido.
La interoperabilidad es uno de los puntos que mas cambia el uso diario. Via FFI se puede llamar a Python, Ruby, R y JavaScript, y la conversion entre Data.Text y las cadenas de Truffle es cero copia para texto codificado en UTF-8. La idea que plantea Kmett: si necesitas un data frame, ejecutar un LLM o pintar algo con D3.js, pasas Text por un foreign import. Los fragmentos de C/C++ de tus librerias van por FFI a Sulong en modo nativo, que es LLVM sobre la JVM. El modo gestionado de Sulong, con punteros bajo recolector, existe, pero no es la ruta que usa el foreign import normal.
Hilos, excepciones y SIMD
Internamente hay dos backends de evaluacion Truffle: uno basado en bytecode y otro en AST, ambos en modo mono o multihilo. Tambien soporta el bytecode de GHC, los BCO que genera GHCi. Cubre throwTo, excepciones asincronas que dejan codigo reanudable, y masking. Sobre Project Loom monta green threads ligeros al estilo de GHC con un ejecutor tipo HEC, lo que abarata los MVar. En SIMD tira de los prim-ops disponibles, que no son muchos, pero ademas elige en runtime la anchura de la "especie" SIMD y compila bucles con esa informacion usando la Vector API que sigue incubandose, jdk.incubator.vector.
Las llamadas de cola calientes se convierten en bucles. Durante el trazado, THC llena un filtro de Bloom de 64 bits para detectar llamadas de cola presumiblemente recursivas; cuando acierta, lanza una excepcion de slow path para enlazar la continuacion con el punto de lanzamiento y hace que Graal convierta el bucle en uno cerrado. Un falso positivo cuesta trabajo extra, pero no cambia el resultado del programa. Cuando el cuerpo crece demasiado, se filtra una llamada de cola y se escapa un marco de pila, que se compacta despues con una tecnica parecida a la recoleccion de basura de CHICKEN Scheme. En las pruebas con Data.Map conto unas 66 llamadas de fallback frente a varios millones de fast path.
El runtime puede usar compressed oops: referencias de 32 bits en lugar de punteros de 64, lo que reduce memoria y ayuda a la cache, a cambio de limitar el heap a unos 32 GB en ese modo. En rendimiento, tras el warmup los numeros se mueven entre 3 veces mas rapido y 3 veces mas lento que GHC, con la mayoria en un 10-20% por debajo. Kmett admite que en los ultimos dias han dejado de medir para ampliar cobertura y han sufrido una regresion de alrededor de 10 veces en benchmarks faciles. Tampoco han comprobado aun si el crecimiento de pila en rutas sin llamada de cola queda acotado respecto a GHC.
El codigo esta en github.com/ekmett/thc, con documentacion de compilacion y uso, y el desarrollo sigue en el canal ##thc de irc.libera.chat. Lo que hay que vigilar ahora es si ese 10-20% por debajo de GHC se recupera y si la compilacion AOT aguanta programas reales, porque de ahi depende que alguien se plantee distribuir un binario hecho con esto.
