ZJIT는 여러 면에서 기존 YJIT와 차별점을 가집니다. 첫째, YJIT가 YARV 바이트코드를 저수준 IR(LIR)로 직접 컴파일하는 반면, ZJIT는 고수준 SSA(Static Single Assignment) 기반 중간 표현(HIR)을 사용합니다. 둘째, YJIT가 한 번에 하나의 기본 블록을 컴파일하는 것과 달리 ZJIT는 전체 메서드를 한 번에 컴파일합니다. 셋째, 타입 프로파일링 방식에서도 YJIT가 지연 기본 블록 버전 관리(LBBV)를 사용하는 것과 달리 ZJIT는 프로파일링된 인터프리터에서 과거 타입 정보를 읽어옵니다. 넷째, 최적화 측면에서는 YJIT가 YARV를 LIR로 내리는 과정에서 최적화를 수행하는 반면, ZJIT는 HIR에서 동작하는 모듈식 고수준 최적화 도구를 갖추고 있습니다. 이러한 핵심적인 차이점은 ZJIT가 커뮤니티의 기여를 용이하게 하기 위해 보다 전통적인 ‘교과서적’ 컴파일러 방식으로 설계되었다는 점입니다.
ZJIT의 컴파일 파이프라인은 YARV 바이트코드를 입력받아 내부적으로 IR을 구축하고 최적화를 거쳐 최종적으로 머신 코드를 생성합니다.
-
YARV (Yet Another Ruby VM): Ruby VM은 Ruby 코드를 스택 기반의 YARV 바이트코드로 컴파일합니다. 예를 들어,
opt_plus
와 같은 연산은 인수의 타입을 확인하고Integer#+
가 재정의되지 않았는지 확인하여 빠른 경로 또는 일반 경로를 선택합니다. 인터프리터에서 함수가 일정 횟수 이상 실행되면 ZJIT는opt_plus
와 같은 Opcode를zjit_opt_plus
와 같은 수정된 버전으로 재작성하여 입력 값의 타입을 기록합니다. -
HIR (High-level Intermediate Representation): HIR은 바이트코드와 달리 그래프 형태를 띠며, 스택 대신 데이터 생성 명령에 직접 포인터로 연결되어 데이터 흐름이 명시적입니다. 초기 HIR은
SendWithoutBlock
과 같은 일반적인 연산을 보여주지만, 최적화 과정을 거치면GuardType
및FixnumAdd
와 같은 타입 특화 코드로 변환됩니다.GuardType
명령은 런타임에 피연산자가Fixnum
인지 확인하며,Fixnum
이 아닐 경우 생성된 코드는 인터프리터로 폴백합니다. 이를 통해FixnumAdd
와 같은 특화된 코드만 생성할 수 있어 효율성을 높입니다. -
LIR (Low-level Intermediate Representation): LIR은 다중 플랫폼 어셈블러의 역할을 하며, 가상 LIR 레지스터를 물리 레지스터 및 스택 위치에 매핑합니다. LIR은 HIR의 고수준 연산을 어셈블리와 유사한 언어로 변환하며,
FrameSetup
,FrameTeardown
,Test
,Jz
,Jo
,Sub
,Add
와 같은 낮은 수준의 명시적인 명령어를 포함하여 HIR보다 더 자세한 실행 과정을 보여줍니다. -
Assembly (ASM): LIR에서 최종적으로 생성되는 머신 코드입니다. 이 어셈블리 코드에서
GuardType
와FixnumAdd
는 각각 몇 개의 매우 빠른 머신 명령만 요구하여 타입 특화의 효율성을 명확히 보여줍니다. ZJIT는 현재 x86뿐만 아니라 ARM 백엔드도 지원하여 다양한 플랫폼에서 활용될 수 있습니다.