Ruby Ractor 로컬 가비지 컬렉션의 도전과제 및 보수적 접근법

[EN] Toward Ractor local GC / Koichi Sasada @ko1

3줄 요약

  • Ruby 3.0에 도입된 Ractor의 병렬 컴퓨팅 이점을 극대화하기 위해 Ractor-local GC 도입이 제안되었습니다.
  • 공유 객체(sharable objects) 처리의 복잡성으로 인해 Ractor-local GC 구현에 어려움이 있었으나, 모든 공유 객체를 항상 살아있는 것으로 간주하는 보수적 접근법이 제시되었습니다.
  • 이 보수적 접근법은 마이크로벤치마크에서 상당한 성능 향상을 보였으며, 향후 Ruby 3.5에 도입될 예정입니다.

Ruby 3.0에서 도입된 Ractor는 병렬 컴퓨팅과 견고한 동시성 프로그래밍을 위한 새로운 모델을 제공합니다. 각 Ractor는 격리된 객체 공간을 가지며, 이는 공유 가변 상태로 인한 일반적인 동시성 버그를 방지하는 데 기여합니다. 그러나 현재 Ruby의 가비지 컬렉션(GC)은 모든 Ractor를 중지시키고 전역적으로 동작하므로, Ractor가 제공하는 병렬 처리의 이점을 충분히 활용하지 못하는 한계가 있습니다. 이러한 병목 현상을 해결하고 병렬 실행 성능을 개선하기 위해 Ractor-local GC의 필요성이 대두되었습니다.

Ractor-local GC를 구현하는 데 있어 가장 큰 도전 과제는 ‘공유 객체(sharable objects)’의 관리입니다. 클래스, 모듈, 클로저 등 여러 Ractor가 참조할 수 있는 공유 객체는 Ractor-local GC가 독립적으로 실행될 때 잘못 해제될 위험이 있습니다. 한 Ractor가 특정 공유 객체를 참조하지 않아 GC 대상이 되더라도, 다른 Ractor가 여전히 해당 객체를 사용하고 있을 수 있기 때문입니다. Ractor 간의 참조 정보를 실시간으로 정확하게 추적하는 것은 병렬 실행 환경에서 매우 어렵고, 이는 성능 저하 및 메모리 오버헤드를 유발할 수 있습니다.

이러한 어려움에 대한 해결책으로, 발표자는 ‘보수적 접근법(conservative approach)’을 제안합니다. 이 접근법은 Ractor-local GC가 실행되는 동안 모든 공유 객체를 항상 살아있는(living) 것으로 가정하고 GC 루트로 취급합니다. 이는 공유 객체 참조의 생존 여부를 복잡하게 추적하는 대신, 안전성을 우선시하는 방식입니다. 이 방법은 공유 객체의 수가 적고 대부분 수명이 긴(long-lived) 특성을 가진다는 점에 착안했습니다. 공유 객체가 더 이상 도달할 수 없게 되더라도, 전역 GC가 실행될 때까지는 메모리에 남아있을 수 있지만, 이는 Ractor-local GC가 안전하게 병렬로 실행될 수 있도록 하는 중요한 절충안입니다.

이 보수적 접근법은 마이크로벤치마크를 통해 그 효과가 입증되었습니다. 짧은 수명 객체 할당, 긴 수명 객체 생성, 정규 표현식 매칭 등 다양한 시나리오에서 Ractor-local GC는 기존 전역 GC 방식에 비해 상당한 성능 향상을 보였습니다. 특히, 16개의 Ractor를 사용했을 때 최대 5배 가까운 속도 향상을 달성했으며, 기존 방식은 병렬 처리 이점을 거의 얻지 못하거나 오히려 성능이 저하되는 현상을 보였습니다. 이는 전역 GC가 모든 Ractor를 중지시키는 데서 오는 병목 현상을 Ractor-local GC가 효과적으로 완화했음을 의미합니다.

결론적으로, Ractor-local GC의 도입은 Ruby의 동시성 및 병렬 처리 성능을 획기적으로 개선할 수 있는 유망한 방향입니다. 공유 객체 관리의 복잡성에도 불구하고, 모든 공유 객체를 살아있는 것으로 간주하는 보수적 접근법은 실용적이고 안전한 첫걸음을 제공합니다. 이 접근법은 "완벽함보다 실행이 중요하다(Done is better than perfect)"는 핵심 통찰을 바탕으로, 현재의 성능 병목을 해결하고 Ruby 3.5에 도입될 주요 기능으로 추진될 예정입니다. 향후 전역 GC의 병렬 마킹 개선 및 더욱 정교한 참조 추적 알고리즘(Solution One)에 대한 연구도 계속될 것입니다.