Ruby Class.new 속도 향상

[EN] Speeding up Class#new - Aaron Patterson

작성자
jeff
발행일
2025년 03월 17일

핵심 요약

  • 1 Ruby의 `Class.new` 메서드 최적화에 대한 발표로, C로 구현된 핵심 기능을 Ruby로 재작성하는 방안을 다룹니다.
  • 2 언어 간 호출 오버헤드를 줄여 전반적인 성능을 향상시키는 데 중점을 둡니다.
  • 3 인라인 캐시와 호출 규약의 중요성을 설명하고, 실제 벤치마크 결과를 통해 개선 효과를 제시합니다.

도입

본 발표는 Ruby 및 Rails 코어 팀 멤버인 Aaron Patterson이 Ruby의 `Class.new` 메서드 최적화에 대한 연구와 그 결과를 공유합니다. 그는 Ruby 코드를 더 많이 작성하는 것의 중요성과 Ruby 언어 자체의 장점(가독성, 작성 용이성, 개발의 즐거움)을 강조합니다. 이 발표의 핵심 목표는 Ruby와 C 코드 간의 전환 오버헤드를 줄임으로써 Ruby의 전반적인 성능을 향상시키는 방안을 모색하는 것입니다.

Aaron Patterson은 Class.new를 최적화 대상으로 선정한 이유를 설명합니다. 이 메서드는 C로 구현되어 있으며, Rails 애플리케이션에서 객체를 생성할 때 매우 빈번하게 호출됩니다. 또한, 그 핵심 로직이 비교적 단순하여 최적화에 적합하다고 판단했습니다. 그는 Class.new의 개념적인 Ruby 구현을 통해 초기 구현의 오류를 지적하고, C 코드와 유사한 올바른 Ruby 로직(객체 할당, initialize 호출, 객체 반환)을 제시합니다.

성능 병목의 주요 원인으로는 Ruby와 C 언어 간의 호출 규약 전환이 지목됩니다. 이러한 언어 경계를 넘나들 때마다 시간 및 메모리 오버헤드가 발생하며, 이는 전체 시스템 성능에 영향을 미칩니다. 발표자는 Ruby 내부에서 실행을 유지함으로써, 비록 개별 Ruby 코드가 C 코드보다 느릴지라도 전환 비용 절감을 통해 전체적인 성능 향상을 이룰 수 있음을 보여줍니다.

인라인 캐시는 메서드 조회를 가속화하는 핵심 메커니즘으로 설명됩니다. 이는 수신자 타입(클래스)을 키로 하여 메서드 엔트리를 저장하며, 바이트코드와 함께 ‘인라인’으로 저장됩니다. 이 캐시는 약한 참조로, 다른 클래스 인스턴스에 동일 메서드를 호출하는 경우 쉽게 무효화될 수 있습니다. 인라인 캐시 미스는 성능을 40%까지 저하시킬 수 있음을 벤치마크를 통해 입증합니다. C 함수가 Ruby 메서드를 호출할 때도 캐시를 사용하지만, 이는 인라인이 아닌 전역 변수에 저장됩니다.

호출 규약(Calling Conventions)은 매개변수와 반환 값이 저장되는 방식을 정의합니다. C는 프로세서에 종속적이고 레지스터 기반인 반면, Ruby는 스택 기반이며 플랫폼 독립적입니다. 이러한 호출 규약의 불일치는 언어 간 마찰을 야기하여, 키워드 인수가 해시로 변환되는 등 추가적인 메모리 할당을 발생시킵니다.

Class.new를 Ruby로 재작성하는 과정에서 초기에는 initialize가 private 메서드라는 점, BasicObjectsend 메서드가 없다는 점 등의 문제에 직면합니다. 해결책으로 Ruby 컴파일러가 특별히 처리하는 ‘프리미티브(Primitive)’를 활용합니다. Primitive.send_delegate를 사용하여 가시성 검사 없이 private 메서드를 호출하고, Primitive.rb_class_alloc2를 통해 메서드 조회를 건너뛰고 객체를 할당합니다. 또한, Primitive.popPrimitive.dup 같은 추가 프리미티브를 사용하여 명령어 수를 8개에서 6개로 줄였으며, 궁극적으로 2~3개까지 줄이는 것을 목표로 합니다.

성능 테스트 결과, 키워드 인수를 사용하는 Class.new의 경우 Ruby 3.5가 3.4보다 2배 빨라졌습니다(할당 횟수 2회에서 1회로 감소). initialize 메서드가 없는 BasicObject의 경우 Ruby 3.5가 12% 느려졌지만, 이는 일반적인 사용 사례가 아니라고 설명합니다. 반면, initialize 메서드가 구현된 클래스의 경우 Ruby 3.5가 14% 더 빨랐습니다. 위치 인수를 사용하는 경우에도 Ruby 3.5는 인수의 수에 관계없이 지속적으로 더 빠른 성능을 보였습니다. 인라인 캐시 미스가 발생하는 극단적인 상황에서도 Ruby 3.5는 3.4와 유사하거나 더 나은 성능을 유지했습니다.

결론

결론적으로, 클래스에 `initialize` 메서드가 구현되어 있다면 새로운 Ruby `Class.new` 구현은 대부분의 경우 더 빠른 성능을 제공합니다. `initialize`가 구현되지 않은 예외적인 경우에만 12% 느려지는 현상이 관찰되지만, 이는 실제 애플리케이션에서 흔치 않은 시나리오로 간주됩니다. 이 발표는 언어 전환 최적화와 Ruby VM 내부 메커니즘 활용을 통해 Ruby가 특정 상황에서 C보다 더 빠를 수 있음을 성공적으로 입증했습니다. Aaron Patterson은 이 발표가 Ruby 내부 구조와 Ruby 언어 자체에 대한 관심을 높이는 계기가 되기를 희망합니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

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