Ractors를 위한 Ruby 인스턴스 변수 접근 최적화

Unlocking Ractors: generic instance variables | byroot’s blog

작성자
발행일
2025년 08월 11일

핵심 요약

  • 1 Ruby Ractor의 병렬 처리 성능 저하를 야기했던 전역 VM 잠금 문제를 해결하기 위해 인스턴스 변수 접근 방식이 개선되었습니다.
  • 2 특히 T_STRUCT와 T_DATA 객체에 대한 인스턴스 변수 접근 시 전역 해시 테이블 대신 직접 참조 방식을 도입하여 성능을 향상시켰습니다.
  • 3 ActiveSupport::SafeBuffer와 Set 객체의 최적화 사례를 통해 Ruby 코어와 라이브러 양단에서 성능 개선이 가능함을 보여주었습니다.

도입

Ruby의 병렬 처리 모델인 Ractor는 이론적으로는 완벽한 병렬성을 목표로 하지만, 실제로는 전역 VM 잠금(Global VM Lock)으로 인해 단일 스레드보다 성능이 저하되는 경우가 많았습니다. 특히 객체 ID 메서드나 클래스 인스턴스 변수와 같은 여러 경합 지점(contention points)이 존재했으며, 이 글은 그중에서도 '제네릭 인스턴스 변수 테이블(generic instance variables table)'이 Ractor 성능에 미치는 영향과 이를 해결하기 위한 다양한 최적화 노력에 대해 다룹니다.

Ruby VM에서 인스턴스 변수는 객체 유형에 따라 다르게 저장됩니다. ‘immediates’는 인스턴스 변수를 가질 수 없으며, T_OBJECT는 객체 슬롯 내에 배열처럼 저장됩니다. T_CLASS와 T_MODULE은 ‘companion’ 슬롯에 인스턴스 변수를 저장합니다. 그러나 String, Array, Hash와 같은 다른 객체 유형(T_STRING, T_ARRAY, T_HASH 등)은 객체 슬롯이 이미 다른 데이터로 사용되므로, 인스턴스 변수를 저장하기 위해 ‘제네릭 인스턴스 변수 해시 테이블(generic_fields_tbl_)’이라는 전역 해시 테이블을 사용합니다. 이 해시 테이블 접근 방식은 해시 조회 비용이 높고, 다중 Ractor 환경에서는 VM 잠금을 획득해야 하므로 심각한 성능 병목 현상을 초래했습니다.

이러한 문제를 해결하기 위해 여러 최적화 방안이 모색되었습니다. 초기에는 T_STRUCT 객체의 인스턴스 변수를 ‘모양(shapes)’으로 인코딩하여 멤버와 변수를 함께 배치하는 아이디어가 있었으나, 복잡한 모양(complex shapes) 처리의 어려움과 Struct 객체의 배열과 같은 특성으로 인해 포기되었습니다. 대신, Struct 객체 슬롯 내의 여유 공간에 인스턴스 변수 버퍼에 대한 직접 참조를 저장하는 ‘직접 참조(Direct References)’ 방식이 도입되었습니다. 이는 T_CLASS와 유사한 전략으로, T_STRUCT 객체에 대한 인스턴스 변수 접근 시 전역 해시 테이블과 VM 잠금을 우회하여 성능을 크게 향상시켰습니다.

T_DATA 객체(C 확장 기능에서 사용)의 경우, 기존 RTypedData 구조체의 ‘typed_flag’ 필드가 1비트 정보를 저장하는 데 8바이트를 비효율적으로 사용하고 있었던 점에 주목했습니다. 최근 Jeremy Evans가 Set 클래스를 C로 재구현하면서 이 2비트 정보를 포인터의 하위 비트(low bits)로 이동시켜 8바이트의 공간을 확보한 사례가 있었습니다. 이 공간을 T_IMEMO/fields에 대한 직접 참조를 저장하는 데 활용하여 T_DATA 객체의 인스턴스 변수 접근 성능을 개선할 가능성이 논의되었습니다.

또한, T_STRING, T_ARRAY, T_HASH와 같이 슬롯 내 여유 공간을 확보하기 어려운 유형에 대해서는 ‘조회 캐시(Lookup Cache)’ 아이디어가 제안되었습니다. 이는 파이버(Fiber) 로컬 저장소에 마지막으로 조회한 객체와 해당 T_IMEMO/fields를 캐시하여 해시 조회 횟수를 줄이는 방식입니다. ActiveSupport::SafeBuffer의 경우, @html_safe = true 대신 @html_unsafe = false로 변경하여 인스턴스 변수 설정을 아예 회피함으로써 String#html_safe 메서드의 성능을 두 배 향상시키는 등, Ruby 코어뿐만 아니라 상위 라이브러 수준에서도 성능 최적화가 가능함을 보여주었습니다.

결론

제네릭 인스턴스 변수 테이블로 인한 경합 문제는 Ruby Ractor의 성능을 저해하는 주요 요인이었습니다. T_STRUCT 객체에 대한 직접 참조 도입과 T_DATA 객체의 메모리 레이아웃 최적화 시도, 그리고 T_STRING, T_ARRAY, T_HASH에 대한 조회 캐시 도입은 전역 VM 잠금 의존도를 줄이고 인스턴스 변수 접근 성능을 향상시키는 중요한 진전입니다. 이러한 최적화는 단일 스레드 애플리케이션과 다중 Ractor 애플리케이션 모두에 긍정적인 영향을 미치며, 향후 Ruby VM에 적절한 동시성 맵(concurrent-map) 구현이 이루어진다면 남은 유형에 대한 락 프리(lock-free) 조회가 가능해질 것으로 기대됩니다. 개발자의 도구에 대한 깊은 이해가 성능 최적화에 얼마나 중요한지를 잘 보여주는 사례입니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

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