Ruby C 확장 코드에서 Write Barrier의 정적 분석 및 자동 삽입 도구: WBCheck

[JA] Write you a Barrier - Automatic Insertion of Write Barriers / @duerst @joetake

작성자
RubyKaigi
발행일
2025년 05월 27일

핵심 요약

  • 1 본 발표는 Ruby의 C 확장 코드에서 가비지 컬렉션(GC) 최적화를 위한 Write Barrier의 복잡성과 중요성을 다룹니다.
  • 2 WBCheck는 C 확장 코드의 정적 분석을 통해 Write Barrier가 필요한 위치를 자동으로 식별하고 삽입하는 실험적인 도구입니다.
  • 3 이 도구는 `TypedData` 객체와 관련된 참조 변경을 추적하여 GC 효율성을 향상시키고 잠재적 오류를 방지하는 데 중점을 둡니다.

도입

Ruby는 동적 메모리 관리를 위해 가비지 컬렉션(GC)을 활용합니다. 특히 세대별 GC는 객체의 수명을 기반으로 효율적인 메모리 회수를 목표로 합니다. 그러나 C 언어로 작성된 Ruby 확장(C Extension)에서는 Ruby 객체를 C 데이터 구조 내에서 다룰 때, GC의 정확성을 보장하기 위해 'Write Barrier'라는 특별한 메커니즘이 필요합니다. Write Barrier는 오래된 객체가 새로운 객체를 참조할 때 이를 GC에 알리는 역할을 하여, 세대별 GC가 참조를 누락하여 살아있는 객체를 잘못 회수하는 문제를 방지합니다. 하지만 C 확장 코드에 Write Barrier를 수동으로 삽입하는 것은 복잡하고 오류 발생 가능성이 높으며, 이는 GC 버그와 같은 심각한 문제로 이어질 수 있습니다. 본 발표는 이러한 문제를 해결하기 위해 C 확장 코드에서 Write Barrier의 필요성을 정적으로 분석하고 자동으로 삽입하는 도구인 'WBCheck'의 개발 배경, 설계, 구현 및 테스트 결과를 소개합니다.

Ruby의 동적 메모리 관리는 힙(Heap) 내의 힙 페이지(Heap Page)와 슬롯(Slot) 단위로 이루어지며, 대부분의 Ruby 객체는 C 언어의 RVALUE 타입으로 관리됩니다. GC의 기본 아이디어는 참조되지 않는 객체를 찾아 메모리를 회수하는 ‘Mark-and-Sweep’ 방식입니다. Ruby의 GC는 효율성을 위해 ‘세대별 GC’를 도입했는데, 이는 새로 할당된 객체(Young Generation)와 오래된 객체(Old Generation)를 구분하여 Minor GC(Young Generation만 대상)와 Major GC(모든 객체 대상)를 수행합니다. 이 과정에서 Old Generation 객체가 Young Generation 객체를 참조하는 경우, Minor GC가 이 참조를 놓칠 수 있어 살아있는 객체가 잘못 회수될 위험이 있습니다. 이를 방지하기 위해 사용되는 것이 바로 Write Barrier입니다. Write Barrier는 Old Generation에서 Young Generation으로의 참조가 발생할 때 이를 ‘Remembered Set’에 기록하여 Minor GC가 해당 참조를 고려하도록 합니다. 또한, GC 처리 중 프로그램 실행을 짧게 중단시키는 ‘Incremental Marking’에서도 참조 변경을 감지하기 위해 Write Barrier가 활용됩니다. Ruby는 일부 unprotected 객체에 대해서는 Write Barrier 없이도 GC가 작동하도록 설계되어 있지만, protected 객체는 Write Barrier가 완벽하게 삽입되어야 합니다.

Ruby C 확장은 C 언어로 Ruby를 조작하거나 C 코드를 Ruby에서 활용할 수 있도록 하는 강력한 메커니즘입니다. C 확장에서는 Ruby 객체가 VALUE 타입으로 다루어지며, 특히 C 데이터 구조를 Ruby 객체로 래핑할 때 사용되는 TypedData(T_DATA) 객체가 중요합니다. TypedData 객체가 다른 Ruby 객체에 대한 참조를 포함하는 경우, 해당 객체를 protected로 만들기 위해 Write Barrier를 반드시 삽입해야 합니다. 그렇지 않으면 GC가 오작동할 수 있습니다. Write Barrier는 주로 RB_OBJ_WRITE() 또는 RB_OBJ_WB() 매크로를 통해 호출됩니다.

WBCheck 도구는 이러한 Write Barrier의 수동 삽입 문제를 해결하기 위해 개발되었습니다. 이 도구는 C 확장 코드에 대한 정적 분석을 수행하여 Write Barrier가 필요한 위치를 자동으로 식별하고, 필요한 경우 직접 삽입합니다. 분석 과정은 다음과 같습니다. 첫째, GCC의 -E 옵션을 사용하여 C 코드 내의 매크로와 디렉티브를 전처리하여 정적 분석의 방해 요소를 제거합니다. 둘째, 오픈 소스 구문 분석 라이브러리인 Tree-sitter와 C99 문법을 사용하여 전처리된 코드를 구문 트리(Abstract Syntax Tree, AST)로 변환합니다. 셋째, 생성된 구문 트리를 두 단계에 걸쳐 분석합니다. 첫 번째 단계에서는 전역 변수, 구조체/공용체 정의, 함수 시그니처 등 표면적인 정보를 수집합니다. 두 번째 단계에서는 함수 본문을 깊이 탐색하여 TypedData 객체의 C 구조체 포인터와 Write Barrier가 필요한 참조 변경(대입, 함수 호출 인자 전달 등)이 발생하는 위치를 식별합니다. 이 과정에서 변수, 구조체 필드 등을 CVar 클래스의 인스턴스로 관리하고, 스코프별로 해시맵을 사용하여 지역 변수를 추적합니다. 특히 RB_CHECK_TYPEDDATA() 호출을 감지하여 TypedData 포인터를 추적하고, 대입이나 memcpy와 같은 참조 변경 연산을 분석합니다.

WBCheck는 Date, JSON, StringIO 라이브러리의 C 확장 코드를 대상으로 테스트되었습니다. 대부분의 필요한 Write Barrier 삽입 위치를 성공적으로 감지했으나, stringio.c의 한 곳을 제외하고는 모두 검출했습니다. 또한, 불필요한 Write Barrier 삽입(예: 초기화 중인 Young Generation 객체에 대한 참조 변경)을 줄이는 개선이 필요함을 확인했습니다. 이 도구는 아직 실험 단계이며, 전처리 자동화, 배열/구조체 필드 내의 포인터 처리, strcpy와 같은 다른 표준 라이브러리 함수 지원 등 개선할 점이 많습니다. 마지막으로, 라이스의 정리(Rice’s Theorem)에 따라 임의의 비자명한 프로그램 속성을 정적 분석만으로 완벽하게 결정하는 것은 불가능하므로, 100%의 정확도 달성은 어려울 수 있음을 명시합니다.

결론

WBCheck는 Ruby C 확장 코드에서 Write Barrier의 필요성을 정적으로 분석하고 자동으로 삽입하는 유망한 도구입니다. 현재까지 대입 및 함수 호출과 관련된 Write Barrier 필요 참조 변경을 성공적으로 감지했으며, 특히 함수 호출이 없는 대입의 경우 Write Barrier 코드를 직접 생성할 수 있는 수준에 도달했습니다. 이 도구는 개발자가 복잡한 Write Barrier 삽입 로직을 수동으로 관리해야 하는 부담을 줄여주고, 잠재적인 GC 관련 오류를 예방하며, Ruby 애플리케이션의 성능과 안정성을 향상시키는 데 기여할 수 있습니다. 향후 지속적인 개선을 통해 더욱 견고하고 정확한 분석을 제공할 수 있을 것으로 기대됩니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

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