Ruby 3.5의 빠른 할당

Fast Allocations in Ruby 3.5

작성자
발행일
2025년 05월 21일

핵심 요약

  • 1 Ruby 3.5에서는 객체 할당 속도가 최대 6배까지 빨라졌으며, 특히 키워드 파라미터에서 두드러진 성능 향상을 보입니다.
  • 2 이러한 속도 향상은 `Class#new` 메서드의 내부 구현을 YARV 명령어 레벨에서 인라인(inlining)하여 달성되었습니다.
  • 3 인라인 기법은 파라미터 복사 제거, 스택 프레임 오버헤드 감소, 그리고 인라인 캐시 적중률 개선을 통해 성능을 최적화합니다.

도입

Ruby 애플리케이션에서 객체 할당은 빈번하게 발생하는 작업이며, 이 과정의 속도는 전체 애플리케이션 성능에 큰 영향을 미칩니다. 본 문서는 Ruby 3.5 버전에서 객체 할당 속도를 획기적으로 개선한 방법에 대해 다룹니다. 특히, 기존 버전 대비 최대 6배까지 빨라진 할당 속도를 벤치마크 결과와 함께 제시하며, 이러한 성능 향상이 어떻게 이루어졌는지 기술적인 세부 사항을 설명합니다.

Ruby 3.5의 객체 할당 속도 개선은 다양한 벤치마크를 통해 입증되었습니다. 벤치마크는 포지셔널(Positional) 및 키워드(Keyword) 파라미터 유형과 YJIT 활성화 여부에 따라 수행되었으며, 파라미터 수가 증가함에 따른 성능 변화도 측정되었습니다. 결과적으로 Ruby 3.5는 Ruby 3.4.2보다 모든 할당 유형에서 더 빨라졌습니다. 포지셔널 파라미터의 경우 파라미터 수에 관계없이 YJIT 비활성화 시 약 1.8배, YJIT 활성화 시 약 2.3배의 일정한 속도 향상을 보였습니다. 특히 키워드 파라미터의 경우 파라미터 수가 증가할수록 속도 향상 폭이 커져, 3개의 키워드 파라미터만으로도 Ruby 3.5는 Ruby 3.4.2보다 3배 빨라졌고, YJIT 활성화 시에는 6.5배 이상 빨라지는 놀라운 결과를 보여주었습니다.

이러한 성능 개선의 핵심은 Class#new 메서드의 최적화에 있습니다. 기존 Class#new는 객체를 할당하고 initialize 메서드를 호출하는 두 가지 주요 작업을 수행했습니다. 문제는 Ruby 함수와 C 함수 간의 호출 규약(calling convention) 불일치로 인해 발생하는 오버헤드였습니다. C 함수는 레지스터나 머신 스택을 통해 파라미터를 전달하는 반면, Ruby는 자체 스택을 사용합니다. 이로 인해 C 함수를 Ruby에서 호출하거나 그 반대의 경우 파라미터 복사 작업이 발생하여 성능 저하의 원인이 되었습니다. 특히 키워드 파라미터는 C 함수로 전달될 때 해시(Hash) 객체로 변환되어 추가적인 할당과 오버헤드를 유발했습니다.

초기에는 Class#new를 Ruby로 재작성하려는 시도가 있었으나, ... (스플랫 파라미터 전달) 문법이 추가 객체 할당을 유발하여 비효율적이었습니다. 이후 ... 최적화를 통해 이 문제를 해결하려 했지만, initialize 호출 지점에서의 인라인 캐시(inline cache) 미스율이 높아 성능상 한계가 있었습니다. CRuby의 단형(monomorphic) 인라인 캐시는 단일 타입만 캐싱할 수 있어, 다양한 타입의 객체가 생성될 때마다 캐시 미스가 발생하고 느린 경로로 메서드를 찾아야 했습니다.

최종적으로 채택된 방법은 Class#new를 YARV 명령어 레벨에서 인라인하는 것이었습니다. 이는 컴파일러가 new 메서드 호출을 발견하면, 실제 new 메서드를 호출하는 대신 new가 수행할 명령어를 호출 지점에 직접 삽입하는 방식입니다. 이로 인해 새로운 opt_new 명령어가 도입되어 객체 할당 및 initialize 호출을 직접 처리하게 됩니다. 이 방식의 주요 장점은 다음과 같습니다:

  • 파라미터 복사 제거: 스택에 푸시된 파라미터가 initialize 메서드에 직접 전달되어 추가적인 복사 작업이 필요 없어집니다.
  • 스택 프레임 오버헤드 제거: Class#new에 대한 스택 프레임 푸시 및 팝 과정이 사라져 오버헤드가 감소합니다.
  • 인라인 캐시 적중률 개선: 모든 new 호출 지점에 initialize 호출 지점이 존재하므로, 순수 Ruby 구현보다 훨씬 높은 캐시 적중률을 기대할 수 있습니다.

물론 이러한 최적화에는 몇 가지 단점도 있습니다. 첫째, 더 많은 명령어가 필요하므로 메모리 사용량이 약간 증가합니다. 하지만 이는 new를 사용하는 호출 지점 수에 비례하며, 전체 힙 크기에 비해서는 미미한 수준입니다. 둘째, 작은 역호환성 문제가 발생합니다. caller 메서드를 통해 호출 스택을 확인하면 Class#new 프레임이 사라진 것을 확인할 수 있습니다. 이는 프레임 자체가 제거되었기 때문입니다.

결론

Ruby 3.5의 객체 할당 속도 개선은 Ruby 애플리케이션의 전반적인 성능을 크게 향상시킬 중요한 변화입니다. 특히 키워드 파라미터 사용 시의 성능 향상은 주목할 만합니다. 이 최적화는 `Class#new`의 인라인 구현을 통해 파라미터 복사 제거, 스택 프레임 오버헤드 감소, 인라인 캐시 적중률 개선이라는 세 가지 주요 이점을 달성했습니다. 비록 사소한 역호환성 문제가 존재하지만, 성능 향상이라는 큰 이점을 고려할 때 Ruby 3.5는 개발자들에게 매우 기대되는 버전이 될 것입니다. 이 작업은 Koichi Sasada의 제안과 John Hawthorn의 구현 기여를 통해 성공적으로 이루어졌습니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

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