루비 FFI 성능 향상을 위한 Tiny JIT 활용 방안

Tiny JITs for a Faster FFI

작성자
발행일
2025년 02월 12일

핵심 요약

  • 1 루비의 FFI는 네이티브 코드 호출 시 성능 오버헤드가 발생하며, C 확장보다 느립니다.
  • 2 저자는 FFI의 오버헤드를 줄이기 위해 런타임에 머신 코드를 생성하는 'Tiny JIT' 개념을 제안합니다.
  • 3 RJIT의 외부 Gem 추출과 FJIT 개념 증명을 통해, FFI를 통한 네이티브 함수 호출이 C 확장보다 빠르고 기존 FFI보다 2배 이상 빨라질 수 있음을 보여줍니다.

도입

루비는 프로그래밍에 있어 많은 장점을 가지고 있지만, 때로는 네이티브 코드 호출이 불가피한 경우가 발생합니다. 이때 사용되는 FFI(Foreign Function Interface)는 C 확장(C extension)이나 루비 자체 메서드 호출에 비해 성능 저하가 발생하는 문제점을 안고 있습니다. 본 글은 이러한 FFI의 성능 한계를 극복하고, 네이티브 코드 호출 시에도 루비 코드를 최대한 활용하는 철학을 유지하면서 성능을 향상시킬 수 있는 'Tiny JIT' 개념과 그 구현 가능성을 탐구합니다.

저자는 strlen C 함수를 벤치마킹하여 FFI, C 확장, 간접 루비 호출, 직접 루비 호출 간의 성능 차이를 명확히 보여줍니다. 벤치마크 결과, String#bytesize 직접 호출이 가장 빠르며, C 확장이 그다음, FFI가 가장 느린 성능을 보였습니다. 이는 FFI를 통한 네이티브 함수 호출 시 상당한 오버헤드가 발생함을 시사합니다.

이러한 오버헤드를 줄이기 위해 저자는 외부 함수 호출에 필요한 코드를 JIT(Just-In-Time) 컴파일하여 머신 코드를 직접 생성하는 아이디어를 제안합니다. 이 아이디어는 attach_function과 같은 FFI 정의 시점에 함수 이름, 파라미터 및 반환 타입 정보를 활용하여 루비 타입을 래핑/언래핑하고 외부 함수를 호출하는 데 필요한 머신 코드를 동적으로 생성하는 것을 목표로 합니다.

이 비전을 실현하기 위해 AArch64Fisk 젬을 통한 머신 코드 생성, JITBuffer 젬을 통한 실행 가능한 메모리 할당, 그리고 가장 중요한 RJIT의 활용이 강조됩니다. RJIT는 루비로 작성된 루비용 JIT 컴파일러로, 최근 외부 젬으로 추출될 예정이며, 이는 서드파티 JIT 컴파일러가 루비의 내부 타입 정보를 얻고, 생성된 머신 코드를 루비가 자동으로 실행하도록 등록할 수 있는 중요한 기반을 제공합니다.

이러한 기술적 기반을 바탕으로 저자는 “FJIT”(FFI JIT)라는 소규모 개념 증명(Proof of Concept)을 개발했습니다. FJIT는 FFI와 유사한 인터페이스를 제공하지만, attach_function 호출 시 필요한 머신 코드를 런타임에 생성합니다. FJIT를 포함한 벤치마크 결과는 놀랍게도 FJIT가 C 확장보다 약간 더 빠르며, 기존 FFI보다 2배 이상 빠른 성능을 보여주며 두 번째로 빠른 성능을 기록했습니다. 이는 JIT 컴파일을 통해 FFI의 성능 병목을 효과적으로 해결할 수 있음을 입증합니다.

결론

이 연구는 루비 개발자가 "가능한 한 루비를 많이 작성"하는 철학을 유지하면서도 C 확장과 동등하거나 그 이상의 네이티브 코드 호출 성능을 달성할 수 있는 매우 흥미로운 가능성을 제시합니다. Zig와 같이 FFI 없이 네이티브 코드 호출을 지원하는 언어의 장점을 루비에서도 누릴 수 있게 될 것입니다. 물론 현재는 ARM64 플랫폼에 한정되고, 모든 파라미터/반환 타입을 지원하지 않으며, 특정 루비 실행 플래그가 필요하다는 등의 제약 사항이 있지만, 저자는 이러한 제약들이 충분히 극복 가능하다고 언급하며 미래 루비 성능 최적화에 대한 기대를 높이고 있습니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

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