YJIT는 어떻게 Ruby를 느리게 함으로써 빠르게 하는가: 디옵티마이제이션의 힘

[EN] Deoptimization: How YJIT Speeds Up Ruby by Slowing Down / Takashi Kokubun @k0kubun

작성자
RubyKaigi
발행일
2025년 05월 27일

핵심 요약

  • 1 YJIT는 '디옵티마이제이션'이라는 독특한 접근 방식을 통해 Ruby의 동적 특성을 유지하면서 성능을 최적화하는 JIT 컴파일러입니다.
  • 2 Ruby 3.4에서는 지역 변수, 싱글톤 클래스, 예외 처리 및 C-Ruby 메서드 전환 등 다양한 영역에서 새로운 디옵티마이제이션 기법이 추가되어 최적화 역량이 강화되었습니다.
  • 3 이러한 예측적 최적화와 필요 시 코드 무효화 전략은 YJIT가 프로덕션 환경에서 실제 성능 향상을 제공하는 핵심 원리입니다.

도입

본 강연은 Shopify의 Ruby 인프라 팀 소속 Kokabun이 Ruby의 JIT 컴파일러인 YJIT가 '디옵티마이제이션(Deoptimization)' 기법을 통해 어떻게 Ruby의 성능을 향상시키는지에 대해 다룹니다. YJIT는 'Yet Another Just-In-Time' 컴파일러의 약자로, 인터프리터가 가상 머신 명령어를 해석하는 대신, 기계어 코드로 직접 변환하여 Intel 또는 ARM CPU에서 네이티브로 실행될 수 있도록 최적화합니다. 현재 YJIT는 Rails 벤치마크에서 인터프리터 대비 두 배 빠른 성능을 보이며, Shopify의 실제 프로덕션 워크로드에서도 평균 18%, 특정 지역에서는 33%의 속도 향상을 입증했습니다. 또한, Rails 7.2 이상 버전과 Ruby 3.3 이상 버전에서는 YJIT가 기본으로 활성화되어 있어, 많은 개발자들이 이미 YJIT의 혜택을 받고 있을 수 있습니다. 강연자는 현재 YJIT 팀이 ZJIT라는 새로운 컴파일러를 개발 중이며, 오늘 논의될 디옵티마이제이션 기술은 ZJIT에도 필수적으로 적용될 것이라고 밝혔습니다.

디옵티마이제이션은 Ruby의 동적인 특성으로 인해 발생할 수 있는 잠재적인 문제점들을 해결하면서도 과감한 최적화를 가능하게 하는 핵심 기법입니다. 이는 ‘언제든지 Ruby를 느리게 만들 수 있다면, 최적화는 무엇이든 될 수 있다’는 철학에 기반합니다. 즉, 예측적으로 최적화를 수행하되, 예측이 빗나가면 해당 최적화를 취소하고 인터프리터로 돌아가는 방식입니다.

기존 YJIT 디옵티마이제이션 (Ruby 3.1부터): * 코드 패칭 (Code Patching): YJIT는 상수(Foo = 1)와 같이 변경되지 않을 것이라고 예상되는 값을 기계어 코드에 인라인하여 최적화합니다. 하지만 Ruby의 상수는 재정의될 수 있으므로, 상수가 재정의되면 YJIT는 해당 기계어 코드를 인터프리터로 점프하는 ‘트램폴린(trampoline)’ 명령으로 패치하여 최적화를 취소합니다. * 전역 무효화 (Global Invalidation): TracePoint와 같은 전역적인 이벤트, 특히 line trace point가 활성화되면 Ruby의 실행 흐름에 심각한 영향을 미칩니다. 이 경우 YJIT는 성능 저하를 방지하기 위해 현재 컴파일된 모든 기계어 코드를 인터프리터로 점프하도록 무효화합니다. 강연자는 프로덕션 환경에서 line trace point 사용을 피할 것을 권장합니다.

Ruby 3.4에 추가된 새로운 디옵티마이제이션: * 이스케이프 로컬 무효화 (Escape Locals Invalidation): binding과 같은 기능을 통해 임의의 메서드가 호출자 프레임의 지역 변수를 변경할 수 있습니다. Ruby 3.4의 YJIT는 지역 변수를 레지스터에 할당하는 최적화(local variable register allocation)를 도입했습니다. 만약 binding이 호출되어 지역 변수가 변경될 가능성이 감지되면, YJIT는 해당 최적화된 코드를 버리고 인터프리터로 전환하여 올바른 동작을 보장합니다. * 싱글톤 클래스 무효화 (Singleton Classes Invalidation): 특정 객체(예: String)에 대한 싱글톤 클래스가 생성되어 해당 객체의 메서드(예: String#+)가 재정의될 수 있습니다. YJIT가 이러한 재정의를 인지하지 못하고 기존의 최적화된 코드를 계속 실행하면 잘못된 결과가 발생할 수 있습니다. Ruby 3.4에서는 String, Array, Hash와 같은 특정 클래스에 싱글톤 클래스가 생성되면, 해당 클래스의 타입 체크 최적화를 무효화하고 인터프리터로 돌아가 정확성을 유지합니다. * 지연 프레임 푸시 (Lazy Frame Push): String#[]=과 같이 예외를 발생시킬 수 있는 메서드의 경우, YJIT는 성능 향상을 위해 메서드 호출 시 프레임 푸시를 건너뛰고 인라인 최적화를 수행할 수 있습니다. 그러나 예외 발생 시 올바른 백트레이스를 위해서는 메서드 프레임이 필요합니다. Ruby 3.4에서는 예외 객체 할당 시 YJIT 훅을 통해 해당 프로그램 위치에 대한 무효화가 등록되어 있는지 확인하고, 필요할 때 프레임을 지연 푸시하여 정확한 백트레이스를 제공합니다. 이는 프로그램 카운터를 기반으로 프레임 내용을 지연적으로 구성하는 방식입니다. * YJIT 전용 메서드 (C Trace Attribute): Ruby는 C로 구현된 메서드와 Ruby로 구현된 메서드 간의 전환 시 성능 오버헤드가 발생할 수 있습니다. Array#each와 같은 핵심 C 메서드를 Ruby로 재구현할 경우, 백트레이스 정보가 변경되어 기존 테스트 케이스를 손상시킬 수 있습니다. Ruby 3.4에 도입된 C trace attribute는 이러한 문제를 해결합니다. YJIT가 활성화되어 Ruby로 구현된 메서드가 실행되더라도, 백트레이스에서는 마치 C 메서드처럼 보이도록 하여 호환성을 유지하고 개발자가 예상치 못한 테스트 실패를 겪지 않도록 돕습니다.

결론

결론적으로, 디옵티마이제이션은 YJIT가 예측적 최적화를 수행하면서도 필요할 때 코드를 지연적으로 무효화하고 버림으로써 Ruby의 동적 특성을 성공적으로 다루는 핵심 기법입니다. Ruby 3.4에 추가된 새로운 디옵티마이제이션 기술들은 지역 변수, 메서드 호출, 싱글톤 클래스 및 예외 처리와 같은 복잡한 시나리오에서 YJIT의 최적화 능력을 한층 더 강화했습니다. 이러한 진보는 YJIT가 실제 프로덕션 환경에서 Ruby 애플리케이션의 성능을 효과적으로 향상시키는 데 기여하고 있음을 보여줍니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

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