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 메서드라는 점, BasicObject
에 send
메서드가 없다는 점 등의 문제에 직면합니다. 해결책으로 Ruby 컴파일러가 특별히 처리하는 ‘프리미티브(Primitive)’를 활용합니다. Primitive.send_delegate
를 사용하여 가시성 검사 없이 private 메서드를 호출하고, Primitive.rb_class_alloc2
를 통해 메서드 조회를 건너뛰고 객체를 할당합니다. 또한, Primitive.pop
과 Primitive.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와 유사하거나 더 나은 성능을 유지했습니다.