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
, 레지스터, 인자, 지역 변수 등을 저장하며, Proc
나 Binding
객체가 생성될 경우 지속성을 위해 힙으로 복사됩니다.
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의 실제 실행을 보여주는 간단한 시연이 있었습니다.