ZJIT: 루비의 차세대 JIT 컴파일러와 성능의 미래

Takashi Kokubun, ZJIT: The future of Ruby performance. San Francisco Ruby Conference 2025.

작성자
Evil Martians
발행일
2025년 12월 27일

핵심 요약

  • 1 ZJIT은 루비 4.0/5.0을 위한 차세대 JIT 컴파일러로, YJIT의 블록 간 최적화 및 메모리 제약 한계를 극복하기 위해 설계되었습니다.
  • 2 ZJIT은 Ruby 언어에 특화된 High-Level Intermediate Representation (HIR)과 SSA(Single Static Assignment) 기반의 메서드 JIT 방식을 도입하여, 더욱 강력한 교차 명령어 최적화를 가능하게 합니다.
  • 3 루비 성능 향상을 위해서는 C-to-Ruby 콜백을 줄이고, 성능에 민감한 부분을 루비 네이티브 코드로 재작성하는 것이 중요하며, ZJIT은 이러한 방향성을 지원합니다.

도입

본 발표는 Ruby 3.1에 통합되어 Shopify 등에서 프로덕션 환경에서 활용되며 10~20%의 성능 향상을 입증한 YJIT에 이어, Ruby 4.0(이전 3.5) 및 5.0에서 도입될 차세대 JIT 컴파일러 ZJIT에 대해 다룹니다. 발표자인 Shopify의 Ruby JIT 팀 소속 Takash Kokubin은 YJIT의 현재 성과를 소개하고, ZJIT이 YJIT의 한계를 어떻게 극복하며 Ruby 성능의 미래를 어떻게 바꿀지 심층적으로 설명합니다. ZJIT은 현재 실험 단계에 있으며, Ruby 4.0에 통합되었지만 본격적인 성능 향상은 Ruby 4.1 이후를 목표로 합니다.

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 + 23으로) 런타임 오버헤드를 줄입니다. 이는 메서드 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

tap` 메서드의 예시처럼, C로 구현된 일부 기능을 Ruby로 재작성함으로써 성능 향상과 코드 단순화를 동시에 이룰 수 있습니다. 또한, 메가모픽(megamorphic) 호출 사이트(단일 호출 지점에서 여러 타겟으로 디스패치되는 경우)도 성능 저하의 원인이 될 수 있습니다.

결론

ZJIT은 Ruby 언어에 특화된 HIR과 메서드 JIT 방식을 통해 YJIT이 달성하기 어려웠던 교차 명령어 최적화를 가능하게 함으로써 Ruby 성능의 새로운 지평을 열고 있습니다. 이를 통해 기존 컴파일러에서 가능했던 다양한 최적화 기법들을 Ruby에 적용할 수 있게 될 것입니다. 궁극적으로 ZJIT의 잠재력을 최대한 활용하기 위해서는, C 확장 기능이 Ruby 코드를 다시 호출하는 경우를 지양하고, 가능한 한 많은 코드를 Ruby로 작성하는 것이 Ruby 성능 향상에 크게 기여할 것이라는 점을 강조합니다. ZJIT은 Ruby의 미래 성능을 위한 중요한 발걸음이며, 향후 더 많은 최적화를 기대하게 합니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!