현재 Class#new
는 C 언어로 구현되어 있으며, 객체 할당 후 initialize
메서드를 호출하는 방식으로 작동합니다. 이 과정에서 Ruby에서 C를 호출하고 다시 C에서 Ruby를 호출하는 언어 간의 전환이 발생하며, 이는 성능 오버헤드를 야기합니다. 이러한 오버헤드를 줄이기 위해 발표자는 인라인 캐시(Inline Cache)와 호출 규약(Calling Convention)이라는 두 가지 핵심 요소를 설명합니다.
인라인 캐시는 메서드 탐색 속도를 높이는 역할을 합니다. Ruby는 메서드를 찾을 때 해당 클래스부터 상위 클래스까지 계층적으로 탐색하는데, 이는 시간이 많이 소요되는 작업입니다. 인라인 캐시는 이전에 탐색했던 메서드의 위치를 캐싱하여 다음 호출 시 빠르게 접근할 수 있도록 돕습니다. 캐시 히트율이 높을수록 성능이 향상되며, 동일한 클래스에서 반복 호출 시 캐시 히트가 발생합니다. 그러나 다른 클래스로 전환하면 캐시 미스가 발생하여 성능이 저하될 수 있습니다. C 언어에서 Ruby 메서드를 호출할 때 사용되는 캐시는 Ruby 프로그램 내부의 인라인 캐시와 달리 전역 테이블에 저장되며, 이는 캐시 제한을 가질 수 있습니다.
호출 규약은 함수 호출 시 인자와 반환 값을 전달하고 찾는 규칙을 의미합니다. Ruby의 VM은 스택을 사용하여 인자를 전달하며, 키워드 인자의 경우 순서를 재배열하는 과정에서 추가적인 비용이 발생할 수 있습니다. C 언어는 키워드 인자를 직접 지원하지 않으므로, Ruby에서 C 함수를 호출할 때 키워드 인자는 해시로 변환되어 전달되고, 다시 C에서 Ruby를 호출할 때는 이 해시를 스택으로 재변환해야 합니다. 이러한 인자 변환 과정 또한 성능 저하의 원인이 됩니다.
발표자는 이러한 문제점들을 해결하기 위해 Class#new
를 Ruby로 재구현하는 새로운 접근 방식을 제안합니다. 초기 시도는 initialize
메서드가 private이라는 문제에 부딪혔고, allocate
를 직접 호출하지 않고 객체를 할당해야 하는 과제가 있었습니다. 이를 해결하기 위해 Ruby 컴파일러가 특별히 처리할 수 있는 새로운 ‘프리미티브(primitive)’를 도입했습니다. 이 프리미티브는 메서드의 가시성 검사를 우회하고, allocate
메서드를 호출하지 않고도 객체를 직접 할당할 수 있도록 합니다. 궁극적인 최적화는 Class#new
의 명령어를 호출자의 바이트코드에 직접 ‘인라인(inline)’하는 것입니다. 이 전략은 Class#new
에 대한 별도의 메서드 호출 자체를 없애, 언어 간 전환 및 호출 규약의 오버헤드를 제거합니다.
성능 벤치마크 결과는 이러한 인라인화 기법이 매우 효과적임을 보여줍니다. 인라인화된 Ruby 3.5는 Ruby 3.4 대비 인자의 개수가 증가할수록 더욱 큰 폭의 성능 향상을 보였습니다. 특히 키워드 인자를 사용할 경우 3개 인자에서 3.2배, 10개 인자에서 6.2배까지 빨라지는 놀라운 결과를 보여주었습니다. 인자의 타입과 개수에 따라 성능 향상 폭은 달라지지만, 최소 1.4배의 속도 향상이 확인되었습니다.
물론 이러한 인라인화에는 단점도 존재합니다. 첫째, 메모리 사용량이 증가합니다. allocate
메서드의 메모리 사용량은 인라인화 후 약 12배 증가했지만, 전체 코드에서 차지하는 비중은 미미하다고 설명합니다. 둘째, 스택 트레이스(stack trace)가 변경됩니다. 인라인화로 인해 Class#new
의 프레임이 스택 트레이스에서 사라지지만, 이는 허용 가능한 수준의 변화로 간주됩니다.