- THC는 GHC가 최적화한 Core를 자체 Truffle/GraalVM 런타임에서 컴파일하고 실행해, Haskell을 JVM에서 구동하는 JIT 컴파일러임
- GHC 9.14.1의 모든 prim-op과 Template Haskell, Linear Haskell을 지원하며, Native Image 기반 AOT 컴파일로 GHC 자체까지 실행 파일로 만들 수 있음
- 다중 언어 FFI로 Python, Ruby, R, JavaScript 라이브러리를 사용할 수 있으며, 다른 언어의 UTF-8 문자열과 Data.Text 사이에는 제로카피 변환을 지원함
- 핫 꼬리 호출을 루프로 변환하고 일반 호출로 쌓인 스택 프레임은 주기적으로 정리함으로써, 꼬리 호출마다 트램펄린을 거치는 방식을 피함
- 개발 초기에는 워밍업 후 성능이 대체로 GHC보다 10~20% 느렸지만, 지원 범위를 확장하는 동안 일부 간단한 벤치마크에서 약 10배의 성능 퇴행이 발생했으며 개선 작업이 진행 중임
- THC(Turbo Haskell compiler) 는 휴가 중 Bartosz Milewski를 방문하면서 장난삼아 시작한 프로젝트로, 개발 시작 일주일 만에 GHC 9.14.1의 모든 prim-op을 구현함
- 타입이 있는 함수형 언어를 실행하기 위해 Cadenza에서 개발한 접근법을 사용하며, 관련 발표도 공개돼 있음
- 실행 기반은 Truffle 과 GraalVM 임
- GHC가 파싱, 타입 검사, 디슈거링, Core 최적화를 담당하고, 이후 THC가 자체 런타임으로 Core를 컴파일하고 실행함
- Template Haskell과 Linear Haskell 같은 고급 언어 기능도 완전히 지원함
- JIT뿐 아니라 Native Image 기반 AOT 컴파일도 지원해 실행 파일을 생성할 수 있음
- pandoc, happy, alex에 이어 게시 시점에는 GHC 자체도 JIT 또는 AOT 컴파일할 수 있음
- Cabal로 패키지를 해석하며, Backpack을 포함해 여러 라이브러리를 가진 패키지를 완전히 지원함
- 다중 언어 FFI로 Python, Ruby, R, JavaScript 라이브러리의 출력을 JIT 컴파일된 Haskell 프로그램으로 바로 가져올 수 있음
- 다른 다중 언어 런타임의 UTF-8 문자열에 대해서는 Data.Text와 Truffle 문자열 사이의 변환이 제로카피로 이루어짐
- 데이터 프레임, LLM 실행, D3.js 시각화가 필요할 때 foreign import로 Text를 전달하는 사용 방식을 지향함
- 준인용(quasi-quotation) 기반의 inline-<language name> 형태 바인딩도 비교적 쉽게 구현할 수 있을 것으로 봄
- Haskell 라이브러리의 C/C++ 코드는 JVM에서 LLVM을 실행하는 Sulong 의 네이티브 모드에 FFI로 연결해 실행함
- LLVM을 JVM 내부에서 해석하고 포인터를 관리하며 가비지 컬렉션하는 관리형 모드도 제공하지만, 일반적인 foreign import 경로에서는 사용하지 않음
- Truffle 평가용으로 바이트코드 기반 JIT와 AST 기반 JIT라는 두 백엔드를 지원함
- 둘 다 단일 스레드와 다중 스레드로 실행할 수 있으며, 다중 스레드에서는 추가 잠금을 사용함
- GHC 바이트코드도 지원하므로 GHCi가 생성한 BCO 코드를 실행할 수 있음
- throwTo, 재개 가능한 코드를 남기는 비동기 예외, 예외 마스킹을 완전히 지원함
- 일반 Java 스레딩과 Project Loom 을 모두 지원함
- 그 위에서 경량 GHC 스타일 그린 스레딩을 제공하며, HEC 스타일 런타임 실행기로 MVar 등을 저비용으로 사용할 수 있게 함
- GHC가 제공하는 제한적인 SIMD prim-op을 지원하는 데 더해, SIMD “species” 폭을 런타임에 선택할 수 있음
- 인큐베이팅 상태인 Vector API, jdk.incubator.vector를 통해 선택한 폭을 반영해 루프를 JIT 컴파일함
- 런타임에서 완전한 RuntimeRep 다형적 코드를 제공하는 데 근본적인 장애물은 없지만, 현재는 그 자유도를 활용하는 Core가 없음
- 자주 실행되는 꼬리 호출은 루프가 되며, 일반 호출로 폴백할 때 쌓이는 스택 프레임은 주기적으로 되감음
- Eta와 Edward Kmett가 Runar Bjarnason과 함께 설계한 Scalaz 모나드는 트램펄린 방식을 사용함
- 이와 달리 THC는 코드 변환으로 핫 꼬리 호출을 측면 출구가 있는 조밀한 기본 블록 형태의 루프 안에 유지함
- 추적 모드에서 재귀가 발생하면 64비트 블룸 필터 를 채워 재귀적 꼬리 호출 가능성을 탐지함
- 일치 가능성이 감지되면 느린 경로 예외를 던져 후속 실행(continuation)을 시작 지점과 연결함
- 맞춤형 Truffle 노드가 Graal을 통해 함수 본문을 가로지르는 꼬리 호출 루프를 조밀한 루프로 변환함
- 거짓 양성은 느린 경로의 작업량만 늘리며 프로그램 결과에는 영향을 주지 않음
- 이후 실행 경로가 갈라지면 추적형 JIT처럼 추가 측면 루프를 확장함
- JVM의 함수 본문 크기 한계에 도달하면 꼬리 호출을 외부로 넘기면서 스택 프레임이 일시적으로 남게 됨
- 누적된 프레임은 CHICKEN Scheme의 가비지 컬렉션 전략 과 다소 유사한 방식으로 압축함
- 비동기 예외가 있어도 코드를 재개하기 위해 마련한 장치를 재사용함
- Data.Map 벤치마크에서는 수백만 번의 빠른 경로 호출에 비해 폴백 트램펄린 호출은 약 66번에 그쳤음
- 런타임은 압축 일반 객체 포인터(compressed oops) 를 사용할 수 있음
- 힙 참조를 64비트 포인터 대신 32비트 오프셋으로 표현해 참조가 차지하는 메모리를 줄이고 더 많은 데이터를 캐시에 넣을 수 있음
- JVM의 일반적인 8바이트 객체 정렬에서는 이 모드의 힙 크기가 약 32GB로 제한됨
- 개발 초기 며칠간 Data.Map 등의 테스트에서는 워밍업 후 성능이 GHC 대비 약 3배 빠른 경우부터 3배 느린 경우까지 분포했고, 대체로 10~20% 느렸음
- 이후 며칠은 지원 범위 확장에 집중하느라 벤치마크를 수행하지 않았으며, 그동안 일부 간단한 벤치마크에서 약 10배의 성능 퇴행이 발생함
- 성능 퇴행을 억제하기 위한 개선 작업을 계속하고 있음
- 꼬리 호출이 아닌 경로의 스택 증가량이 GHC의 스택 사용량에 비례하는 범위 안에 머무는지는 아직 테스트하지 않음
- 이 상한을 보장하는 작업은 향후 과제로 남아 있음
- THC 코드 와 빌드, 실행, 사용 방법을 다루는 문서 가 공개돼 있음
- 개발 논의는 irc.libera.chat의 ##thc 채널 에서 진행함