Turbo Haskell
Turbo Haskell — GHC Core를 JVM에서 실행하는 컴파일러
Edward Kmett가 만든 THC는 GHC의 프런트엔드와 Core 최적화를 유지하면서, GHC Core를 Truffle·GraalVM 위에서 JIT 또는 AOT 컴파일하는 프로젝트입니다. GHC 9.14.1의 prim-op와 여러 고급 기능을 지원하며, Haskell 프로그램은 물론 GHC 자체도 실행합니다. 성능은 워밍업 뒤 GHC보다 대체로 10~20% 느렸지만, 최근 회귀가 발생해 개선 중입니다.
- 주제
AI 요약
Edward Kmett는 Bartosz Milewski를 방문하던 휴가 중 농담 삼아 Turbo Haskell Compiler, 즉 THC를 만들기 시작했습니다. 일주일 뒤 THC는 GHC 9.14.1의 prim-op 전체를 구현하고, GHC Core를 JVM에서 실행하는 JIT를 갖췄습니다. 기반 기술은 Kmett가 몇 년 전 Cadenza에서 개발한 방식으로, Truffle과 GraalVM을 이용해 타입이 있는 함수형 언어를 실행합니다.
GHC와 THC의 역할
GHC는 파싱, 타입 검사, desugaring, Core 최적화를 담당합니다. 그 뒤 THC가 Core를 넘겨받아 자체 런타임과 Truffle·GraalVM으로 컴파일하고 실행합니다. Template Haskell과 Linear Haskell 같은 고급 기능도 지원합니다. JIT뿐 아니라 GraalVM Native Image를 이용한 AOT 컴파일도 제공해 실행 파일을 만들 수 있습니다. pandoc, happy, alex 같은 프로그램을 컴파일하며, 최근에는 GHC 자체도 실행했습니다.
패키지는 Cabal로 해석합니다. 여러 라이브러리를 포함한 패키지와 Backpack도 지원합니다. GHC 바이트코드도 실행하므로 GHCi가 만든 BCO 코드도 처리합니다.
다른 언어와 라이브러리 연결
THC는 Python, Ruby, R, JavaScript와 연결하는 다언어 FFI를 제공합니다. 다른 언어의 라이브러리를 Haskell 프로그램에서 호출하고, 결과를 JIT 컴파일된 코드로 가져오는 방식입니다. UTF-8 문자열이라면 FFI 경계에서 Data.Text와 Truffle 문자열을 변환할 때 복사하지 않습니다.
예를 들어 데이터 프레임이나 LLM, D3.js 시각화가 필요하면 foreign import를 거쳐 Text를 다른 언어로 전달하는 구상을 제시합니다. Quasi-quotation을 활용한 인라인 언어 바인딩도 구현하기 어렵지 않을 것으로 봅니다. Haskell 패키지 안의 C/C++ 코드는 JVM에서 LLVM을 실행하는 Sulong의 native mode를 통해 FFI로 호출합니다. LLVM을 JVM 안에서 해석하고 포인터를 관리·가비지 컬렉션하는 managed mode도 있지만, 일반 foreign import 경로에서는 사용하지 않습니다.
평가와 동시성
Truffle 평가에는 두 백엔드가 있습니다. 하나는 바이트코드 기반 JIT이고, 다른 하나는 전통적인 AST 기반 JIT입니다. 둘 다 단일 스레드와 멀티스레드 방식으로 실행하며, 멀티스레드에서는 추가 잠금 처리를 합니다. 비동기 예외를 발생시키는 throwTo도 지원합니다. 예외가 중단 가능한 코드를 남기고, masking 기능도 제공합니다.
일반 Java 스레드와 Project Loom을 모두 지원합니다. Loom 위에서는 GHC 스타일의 경량 green thread를 제공하고, HEC 스타일 런타임 실행기로 MVar 같은 동시성 구조를 낮은 비용으로 처리합니다.
꼬리 호출과 SIMD
자주 실행되는 꼬리 호출은 루프로 바뀝니다. 일반 호출로 돌아가야 할 때는 누적된 스택 프레임을 주기적으로 풀어냅니다. Eta나 Scalaz 모나드의 trampoline 방식과 달리, THC는 코드 변환으로 자주 실행되는 꼬리 호출을 기본 블록 형태의 촘촘한 루프 안에 둡니다.
Tracing 모드에서 재귀가 일어나면 THC는 64비트 Bloom filter를 채워 꼬리 재귀 호출 가능성을 감지합니다. 가능성이 높은 호출을 찾으면 느린 경로의 예외를 던져 continuation과 시작 지점을 연결합니다. 이어 custom Truffle 노드가 여러 함수 본문에 걸친 꼬리 호출 루프를 하나의 루프로 Graal에 변환하도록 합니다. Bloom filter의 오탐은 느린 경로 작업을 늘릴 뿐 프로그램 결과는 바꾸지 않습니다. 실행 경로가 갈라지면 tracing JIT처럼 추가 side loop를 만들고, JVM의 함수 본문 크기 한계에 이르면 꼬리 호출에서 스택 프레임을 남깁니다. 쌓인 프레임은 비동기 예외의 재개 가능한 코드에 필요한 구조를 재사용해 압축합니다. Data.Map 벤치마크에서는 빠른 경로 호출 수백만 건에 비해 fallback trampoline 호출이 약 66회였습니다.
SIMD는 GHC prim-op만으로 지원하는 데 그치지 않습니다. 실행 중 SIMD 폭을 고르고, 그 값을 사용해 Vector API(jdk.incubator.vector) 기반 루프를 JIT 컴파일합니다. 런타임에서 RuntimeRep 다형 코드를 제공하는 데 막힌 부분은 그런 자유를 활용하는 Core가 아직 없다는 점뿐이라고 설명합니다.
성능과 현재 상태
THC 런타임은 compressed ordinary object pointers, 즉 compressed oops도 지원합니다. 힙 참조를 64비트 포인터 대신 32비트 오프셋으로 표현해 참조가 차지하는 메모리를 줄이고 캐시에 더 많은 데이터를 담습니다. JVM의 일반적인 객체 정렬이 8바이트라 이 모드에서는 힙 크기가 대략 32GB로 제한됩니다.
개발 초기에는 성능을 주요 고려 사항으로 삼았습니다. Data.Map 등에서 워밍업 뒤 성능은 GHC보다 3배 빠른 경우부터 3배 느린 경우까지 분포했고, 대체로 GHC보다 10~20% 느렸습니다. 다만 최근 며칠은 기능 범위를 넓히느라 벤치마크를 진행하지 않았고, 쉬운 일부 벤치마크에서 약 10배 성능 회귀가 생겼습니다. 이를 억제하는 작업을 이어가고 있습니다. 꼬리 호출이 아닌 경로에서 스택이 GHC와 비교해 계속 제한된 크기로 유지되는지는 아직 시험하지 않았습니다.
코드는 github.com/ekmett/thc에서 공개하며 빌드와 실행, 사용법 문서도 제공합니다. 개발 논의는 irc.libera.chat의 ##thc 채널에서 진행합니다.
원문: comonad.com / 번역·요약: Trawling