Monoruby: Ruby 성능 최적화를 위한 새로운 JIT 컴파일러

[EN] Improving my own Ruby / monochrome @s_isshiki1969

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

핵심 요약

  • 1 Monoruby는 Ruby의 새로운 구현체로, JIT 컴파일러와 특수화(Specialization) 기법을 활용하여 CRuby 대비 3~10배 빠른 성능을 목표로 합니다.
  • 2 Bignum, Fiber 등 핵심 기능을 지원하지만 C 확장, 네이티브 스레드 등은 지원하지 않아 호환성 문제가 있습니다.
  • 3 효율적인 레지스터 관리와 바이트코드 추적 정보를 통해 동적 언어의 특성을 극복하고 고성능 머신 코드를 생성합니다.

도입

본 발표는 모노그램(Monogram)이 수년간 개발해 온 Ruby 구현체인 Monoruby에 대해 다룹니다. Monoruby는 기존 Ruby의 파서, 가비지 컬렉터, 인터프리터를 재구현한 것으로, 특히 성능 향상에 초점을 맞추고 있습니다. Monoruby는 현재 Gems 지원을 진행 중이며, Bundler와의 호환성 확보를 위해 노력하고 있습니다. 발표자는 외과의사라는 특이한 배경을 가지고 있으며, Matama 시티와의 개인적인 인연을 언급하며 발표를 시작합니다.

Monoruby는 CRuby와의 호환성을 위해 Bignum, Fiber, Binding, 기본 연산 재정의 등을 지원합니다. 그러나 C 확장, 네이티브 스레드, ObjectSpace, TracePoint, Refinements, CallCC 등은 지원하지 않는데, 특히 C 확장 미지원은 큰 제약 사항으로 꼽힙니다. CallCC는 향후에도 지원하지 않을 예정입니다.

성능 벤치마크 결과, Monoruby는 마이크로벤치마크(whitebench)에서 CRuby 인터프리터 대비 3~10배 빠른 성능을 보였으며, CRuby JIT 컴파일러와는 비슷한 수준의 성능을 나타냈습니다. 광학 카운트 벤치마크에서는 인터프리터 대비 6~7배 빠른 프레임 속도를 달성했습니다. Monoruby는 시작 속도가 느리지만, 일단 실행되면 높은 성능을 보여줍니다.

Monoruby는 디버깅 및 프로파일링을 위한 다양한 빌드 옵션을 제공하여 디옵티마이제이션, 재컴파일, 메서드 캐시 실패 등의 통계를 확인할 수 있습니다. 바이트코드 구조는 16 또는 32바이트 길이의 명령어로 구성되며, 각 바이트코드에는 인터프리터가 수집한 추적 정보(예: 클래스 ID, 메서드 버전)가 포함되어 인라인 메서드 캐시와 같은 최적화에 활용됩니다.

스택 프레임은 호출된 메서드 블록마다 푸시되고 반환 시 팝됩니다. 각 프레임은 호출자 프레임과 로컬 프레임에 연결됩니다. 로컬 프레임은 self, 레지스터, 인자, 지역 변수 등을 저장하며, ProcBinding 객체가 생성될 경우 지속성을 위해 힙으로 복사됩니다.

Monoruby의 핵심은 인터프리터와 JIT 컴파일러의 유기적인 연동입니다. 인터프리터는 바이트코드를 읽고 실행하는 무한 루프를 수행하며, 특정 메서드 호출이나 루프가 빈번하게 실행될 경우 JIT 컴파일러가 바이트코드를 머신 코드로 컴파일합니다. JIT 코드는 특정 가정(예: 수신자 객체의 클래스)하에 생성되며, 이 가정이 깨지면 디옵티마이제이션(인터프리터로 폴백)이 발생합니다. JIT 컴파일러는 복잡한 시스템이지만, 성능 향상이라는 단 하나의 목적을 위해 존재합니다.

성능 최적화를 위해 JIT 컴파일러는 CPU의 레지스터(범용, 부동 소수점)를 효율적으로 사용합니다. 컴파일러는 각 레지스터의 값이 런타임에 어디에 저장되는지(스택, CPU 레지스터, 부동 소수점 레지스터)를 추적하여 최적의 코드를 생성합니다. 예를 들어, 원의 면적을 계산하는 area 메서드에서 JIT 컴파일러는 인자의 타입을 추적하고, 정수/부동 소수점 연산을 위한 레지스터를 적절히 활용하며, 오버플로우 체크와 Ruby 값으로의 변환을 처리합니다.

또 다른 중요한 최적화 기법은 특수화(Specialization)입니다. each와 같이 블록을 인자로 받는 메서드의 경우, 컴파일 시점에 주어진 블록의 시그니처(인자 수, 종류)를 알 수 없어 비효율적인 코드가 생성될 수 있습니다. Monoruby는 호출자 메서드와 블록을 한 번에 컴파일하고, each와 같은 메서드를 Rust가 아닌 Ruby로 작성하여 정보를 효율적으로 사용합니다. 이를 통해 block_given?, Array#size, Array#[] 등에서 불필요한 조건 분기나 명령어를 제거하고 인라인 어셈블리를 삽입하여 성능을 향상시킵니다. 특히 yield는 컴파일 시점에 콜리 블록의 시그니처를 알 수 있어 효율적인 코드를 생성할 수 있습니다. 특수화 벤치마크 결과, Integer#times, Integer#step, Array#map 등에서 Monoruby의 성능이 크게 향상되었음을 보여줍니다. Ruby로 작성된 Monoruby 메서드가 Rust로 작성된 최적화 전 메서드보다 더 나은 성능을 보였습니다.

발표의 마지막에는 Monoruby의 실제 실행을 보여주는 간단한 시연이 있었습니다.

결론

결론적으로 Monoruby는 JIT 컴파일과 특수화라는 두 가지 핵심 최적화 기법을 통해 Ruby 코드의 실행 속도를 획기적으로 향상시키는 데 성공했습니다. 이는 동적 언어인 Ruby의 특성으로 인한 런타임 오버헤드를 줄이고, 더 효율적인 머신 코드를 생성함으로써 가능해졌습니다. 아직 C 확장 호환성 등의 과제가 남아있지만, Monoruby는 Ruby의 성능 지평을 넓히는 중요한 시도이자, 미래 Ruby 런타임 개발에 기여할 잠재력을 보여주고 있습니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

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