YJIT의 한계와 ZJIT의 등장 배경
YJIT은 Ruby 3.1에 통합된 JIT 컴파일러로, 인터프리터 대비 최대 2배 빠른 성능을 보여주며 Shopify와 Rails 7.2에서 기본 활성화되는 등 긍정적인 평가를 받고 있습니다. 그러나 YJIT은 ‘블록 로컬(block local)’ 최적화 방식에 집중하여, 기본 블록(basic block) 내에서만 최적화를 수행하고 블록 간 교차 명령어 최적화에는 한계가 있었습니다. 이는 최적화를 위해 더 많은 메모리를 사용해야 하는 부담이 있었기 때문입니다.
ZJIT의 핵심 설계: HIR과 메서드 JIT
ZJIT은 이러한 YJIT의 한계를 극복하기 위해 새로운 디자인을 채택했습니다. 핵심은 다음과 같습니다.
-
프로파일링 방식 개선: ZJIT은 런타임 타입 프로파일링을 위해
zjit_접두사가 붙은 특수 명령어를 사용합니다. 특정 임계값에 도달하면 이 명령어를 통해 프로파일 정보를 수집하고, 이를 바탕으로 최적화를 진행합니다. -
High-Level Intermediate Representation (HIR): ZJIT은 Ruby 언어에 특화된 HIR을 도입합니다. 이는 LLVM과 같은 범용 IR이 Ruby의 C-Ruby 가상 머신(CVM) 컨텍스트를 이해하고 최적화하기 어려운 점을 해결하기 위함입니다. HIR은 SSA(Single Static Assignment) 형태로 변수를 한 번만 할당하여 최적화를 용이하게 합니다.
-
메서드 JIT 방식: YJIT이 블록 단위로 컴파일하는 것과 달리, ZJIT은 메서드 전체를 한 번에 컴파일하는 ‘메서드 JIT’ 방식을 사용합니다. 이를 통해 메서드 내 모든 컨텍스트와 그래프를 파악하여 블록 간 최적화(cross-block optimization)를 효과적으로 수행할 수 있습니다.
ZJIT의 최적화 패스
ZJIT은 HIR을 기반으로 다양한 최적화 패스를 적용합니다.
-
Type Specialization: 프로파일링된 타입 정보를 활용하여 메서드 호출을 특정 C 함수 호출이나 상수 반환 등으로 대체합니다.
-
Inlining: 메서드 인라이닝을 통해 여러 메서드의 코드를 통합하여 최적화 범위를 넓힙니다.
-
Fold Constants: 컴파일 타임에 상수 값을 미리 계산하여(예:
1 + 2를3으로) 런타임 오버헤드를 줄입니다. 이는 메서드 JIT 방식을 통해 전체 컨텍스트를 파악할 수 있기에 가능합니다. -
Dead Code Elimination: 사용되지 않는 변수나 명령어를 제거하여 생성되는 머신 코드의 크기를 줄이고 효율성을 높입니다.
-
Register Allocation: Low-Level IR 단계에서 CPU 레지스터를 효율적으로 할당하여 메모리 접근을 줄이고 실행 속도를 향상시킵니다.
ZJIT 사용 및 실험
ZJIT을 사용하려면 Ruby 빌드 시 --enable-zjit 플래그를 활성화하고, 런타임 시 -ZJIT 옵션을 사용해야 합니다. HIR을 시각화하려면 -ZJIT-dump-hir 옵션을 활용하거나, tzg.fly.dev 웹사이트에서 인터랙티브하게 확인할 수 있습니다.
루비 성능의 미래와 C 확장
ZJIT 시대에 Ruby 성능을 극대화하기 위해서는 ‘C-to-Ruby 콜백’을 줄이는 것이 중요합니다. C 확장 기능이 Ruby 메서드를 호출하는 경우, 인터프리터 프레임 설정 및 스택 저장 등으로 인해 상당한 성능 저하가 발생합니다. 따라서 데이터베이스 바인딩과 같이 Ruby에서 C를 호출하는 경우는 괜찮지만, C 확장 내부에서 Ruby 코드를 다시 호출해야 한다면 해당 부분을 Ruby로 재작성하는 것이 권장됩니다. `Kernel