Ruby의 기존 가비지 컬렉터(GC)
Ruby는 Mark-Sweep-Compact 알고리즘을 사용하는 GC를 채택하고 있습니다. 이 GC는 객체 할당, 생존/소멸 객체 판별, 그리고 소멸 객체 회수의 세 가지 주요 역할을 수행합니다. * Mark 단계: 루트 객체(전역 변수, 상수 등)에서 시작하여 참조되는 모든 객체를 추적하여 ‘살아있는(alive)’ 객체로 표시합니다. * Sweep 단계: Mark 단계에서 살아있는 것으로 표시되지 않은 객체들을 ‘소멸(dead)’ 객체로 간주하고 메모리를 회수합니다. 이 과정에서 파일 디스크립터 닫기, 파이널라이저 실행 등 객체에 할당된 추가 리소스도 정리합니다. * Compact 단계 (선택 사항): 메모리 단편화(fragmentation)를 줄이기 위해 살아있는 객체들을 힙의 한쪽 끝으로 이동시킵니다. Ruby는 이를 위해 Two-finger 알고리즘을 사용합니다. 이러한 방식은 구현이 간단하고 이해하기 쉬우며 안정적이지만, 성능 최적화를 위한 고급 GC 알고리즘을 적용하는 데는 한계가 있습니다.
기존 GC의 한계와 대안 알고리즘
Ruby의 기존 GC는 지난 30년간 개선되어 왔지만, 다음과 같은 기능들이 부족합니다.
* 참조 카운팅(Reference Counting): 객체 참조 수를 추적하여 참조가 0이 되면 즉시 회수하는 방식입니다. Ruby의 write barrier
구현이 불완전하여 현재는 적용하기 어렵습니다.
* 세미-스페이스 복사(Semi-Space Copying): 메모리를 두 영역으로 나누어 살아있는 객체만 다른 영역으로 복사하여 단편화를 제거합니다. 메모리 사용량이 두 배로 늘어나고, Ruby가 요구하는 객체 고정(pinning)을 지원하지 못해 적용이 불가능합니다.
* IMMIX: Mark-Sweep과 Semi-Space Copying의 아이디어를 결합한 알고리즘입니다. 힙을 블록으로 나누고, 단편화가 심한 블록의 살아있는 객체만 이동시키며, 객체 고정을 지원하여 Ruby에 적용 가능성이 높습니다.
* 병렬 처리(Parallelism): 현재 Ruby GC는 단일 스레드로 동작하여 멀티 코어 CPU의 이점을 활용하지 못하며, Ractor 사용 시 병목 현상의 원인이 됩니다. 병렬 GC는 성능 향상에 필수적입니다.
* 동적 슬롯 크기(Dynamic Slot Sizes): Ruby 3.2에서 가변 폭 할당(variable with allocation)을 도입하여 메모리 효율성을 개선했지만, 40바이트의 거듭제곱 형태인 5가지 고정된 슬롯 크기(40, 80, 160, 320, 640바이트)로 제한됩니다. 완전한 동적 슬롯 크기 지원은 더 큰 성능 및 메모리 효율성 개선을 가져올 수 있습니다.
모듈형 가비지 컬렉터 (Modular GC)
이러한 한계를 극복하기 위해 Ruby는 기존 GC를 대체하는 대신, 사용자가 필요에 따라 GC 구현체를 전환할 수 있는 모듈형 가비지 컬렉터를 도입했습니다.
* 도입 배경: 모든 워크로드에 맞는 ‘만능’ GC는 없으며, 기존 GC의 안정성을 유지하면서 새로운 알고리즘의 유연성을 확보하기 위함입니다.
* Ruby 3.4의 실험적 기능: GC 구현을 위한 API와 Ruby에 로드하는 메커니즘을 제공합니다.
* 구조 변화: 기존 GC.c
파일을 VM 인터페이스(GC.c
)와 실제 Mark-Sweep GC 구현(gc/default.c
)으로 분리했습니다. GC 구현체는 공유 라이브러리 형태로 Ruby에 로드됩니다.
* Modular GC API: GC 구현체가 반드시 구현해야 하는 약 60개의 함수(부팅, 종료, GC 주기 실행, 객체 할당, 마킹, 고정, write barrier 등)와 Ruby VM이 GC 구현체에 제공하는 함수(VM 잠금/해제, Ractor 중지, 객체 참조 순회, 객체 정리 등)로 구성됩니다.
MMTK (Memory Management Toolkit) 통합
MMTK는 Modular GC API를 활용하여 Ruby에 통합된 최초의 외부 GC 구현체입니다. * MMTK란?: Rust로 작성된 언어 독립적인 가비지 컬렉터 프레임워크로, 다양한 GC 알고리즘(Plans)을 제공합니다. Java(Open JDK), Julia, V8(JavaScript) 등 여러 언어에 통합되어 있습니다. * MMTK의 주요 기능: * 다양한 GC 알고리즘: No GC, Mark-Sweep, Mark-Compact, Semi-Space Copying, IMMIX, LXR (Latency Critical Immix with Reference Counting) 등을 지원합니다. LXR은 참조 카운팅과 IMMIX를 결합하여 낮은 지연 시간과 높은 성능을 제공합니다. * 병렬 처리: Work Packet 개념을 사용하여 GC 작업을 여러 스레드에서 병렬로 처리할 수 있도록 설계되었습니다. * MMTK와 Ruby의 통합: * MMTK 팀이 2020년부터 Ruby 포크를 통해 구현 작업을 진행해왔습니다. * Modular GC 도입 후, 기존 MMTK 구현의 아이디어를 Modular GC API 위로 포팅하는 작업이 진행 중입니다. 현재 No GC, Mark-Sweep, Non-moving IMMIX가 구현되었습니다. * MMTK 바인딩은 C 레이어(Modular GC API와 상호작용)와 Rust 레이어(MMTK 코어와 상호작용)로 구성됩니다. * 향후 로드맵: 현재 MMTK 기반 Ruby GC는 Ruby 기본 GC보다 느리지만, Moving Plan 지원, 세대별 GC(Generational GC) 지원(Sticky IMMIX 등), Work Packet 세분화를 통한 병렬 처리 극대화, 그리고 GC 내부의 스레드 안전성 확보를 통해 성능을 크게 개선할 계획입니다.