최근 RubyKaigi 2025 발표와 Matz의 승인에 따라, 새로운 JIT(Just-In-Time) 컴파일러인 ZJIT가 Ruby의 레퍼런스 구현체인 YARV에 공식적으로 병합되었습니다. ZJIT는 기존 YJIT 컴파일러를 개발한 팀이 주도하며, 올해 초부터 개발이 진행되어 왔습니다. 본 문서는 개발 초기 단계인 ZJIT 프로젝트의 개요를 고수준에서 설명합니다. ZJIT는 YJIT와 여러 면에서 차이를 보이며, 특히 커뮤니티 기여를 쉽게 만들기 위해 보다 전통적인 '교과서적인' 컴파일러 아키텍처를 의도적으로 채택하고 있습니다.
ZJIT는 YARV 바이트코드를 입력받아 중간 표현(IR)을 구축하고, 최적화를 거쳐 최종적으로 머신 코드를 생성하는 파이프라인을 가집니다. 이 과정은 YARV -> HIR -> LIR -> ASM의 흐름으로 진행됩니다. 먼저, Ruby 코드는 YARV 바이트코드로 컴파일됩니다. YARV는 스택 기반의 가상 머신으로, 대부분의 명령어는 스택에서 입력을 팝하고 결과를 푸시합니다. 예를 들어, add
메서드의 opt_plus
명령어는 피연산자를 스택에서 읽어 덧셈을 수행하며, Integer#+
의 재정의 여부를 확인하는 ‘bop check’를 포함합니다.
인터프리터에서 함수가 일정 횟수 실행되면, ZJIT는 일부 오피코드를 zjit_opt_plus
와 같이 수정하여 인자 타입을 프로파일링합니다. 추가 실행 후, ZJIT는 함수를 컴파일합니다. 컴파일 파이프라인의 첫 단계는 HIR(High-level SSA-based Intermediate Representation) 생성입니다. YARV 바이트코드가 간결하고 스택 기반인 반면, HIR은 그래프 형태이며 SSA(Static Single Assignment) 기반으로 데이터 흐름이 명시적입니다. 바이트코드의 opt_plus
는 HIR에서 제네릭 SendWithoutBlock
으로 변환됩니다.
HIR은 최적화 파이프라인을 거치면서 크게 변화합니다. 예를 들어, add
함수는 타입 특화된 코드로 변환되어, 피연산자가 Fixnum
인지 런타임에 확인하는 GuardType
명령어와 실제 덧셈을 수행하는 FixnumAdd
가 삽입됩니다. GuardType
체크 실패 시에는 인터프리터로 폴백하는 사이드 엑시트(현재는 개발 중)가 사용됩니다.
다음 단계는 LIR(Low-level Intermediate Representation)입니다. LIR은 다중 플랫폼 어셈블러를 목표로 하며, 레지스터 할당 기능을 제공합니다. HIR의 고수준 연산은 어셈블리 유사 언어로 변환됩니다. HIR이 여러 기본 블록을 가질 수 있는 반면, LIR은 하나의 선형 블록입니다. LIR에서는 FrameSetup
, FrameTeardown
, Test
, 조건부 점프(Jz
, Jo
), Sub
, Add
와 같은 저수준 연산을 확인할 수 있습니다.
마지막으로, LIR은 특정 아키텍처의 어셈블리(ASM) 코드로 변환됩니다. GuardType
와 FixnumAdd
는 어셈블리 수준에서 단 몇 개의 빠른 머신 명령어로 구현되어 타입 특화의 효율성을 보여줍니다. ZJIT는 x86 및 ARM 백엔드를 지원합니다.
ZJIT 프로젝트는 아직 초기 개발 단계에 있습니다. 현재 프로덕션 환경에서의 사용은 권장되지 않으며, Ruby 3.5에는 YJIT와 ZJIT가 모두 포함될 예정입니다. 개발팀은 ZJIT가 YJIT와 동등한 기능 및 성능을 갖출 때까지 지속적으로 개선할 계획입니다. 특히, 런타임 타입 불일치 시 인터프리터로 안전하게 전환하는 사이드 엑시트 구현이 현재 주요 개발 목표입니다. 사이드 엑시트가 구현되면 Ruby 테스트 스위트 및 실제 애플리케이션 벤치마크를 실행하여 정확성과 성능 기준을 확보할 수 있습니다. 이후 프로파일링을 통해 가장 효과적인 최적화에 집중할 것입니다. ZJIT는 커뮤니티 기여를 염두에 둔 설계 철학을 가지고 있으며, 앞으로 더 많은 정보와 문서가 공개될 예정입니다.