TL;DR
- Mark Shannon은 Python의 가비지 컬렉션(GC)에 세대별·증분 방식을 결합해 메모리 사용량과 일시 정지 시간을 함께 개선하는 방안을 제안함.
- Python 3.14에 포함됐다가 되돌려진 증분 GC는 최대 정지 시간을 기존 세대별 GC의 약 3초에서 수십 밀리초로 줄였지만, 프로덕션 환경에서 상당한 메모리 압박이 보고됨.
- Python 실행 시간의 약 11.67%를 차지하는 GC는 참조 카운팅을 보완하는 역할이며, 두 GC의 회수 효율은 각각 세대별 GC 0.3%, 되돌려진 증분 GC 약 1%임.
- 제안된 설계는 젊은 세대 20MB, 오래된 세대와의 번갈아 수행, 젊은 세대 생존율의 두 배 빈도로 오래된 세대 수집을 포함함.
- 참가자들은 GC 조정 기능, 기존 애플리케이션 호환성, 테스트 범위와 동시 GC의 구현 난도를 논의했으며, Mark Shannon은 결정 전에 실제 사용 데이터를 살펴보자는 입장임.
발표 배경과 GC의 역할
- 세 번째 Python Language Summit 발표는 Python 3.14에 포함된 증분 가비지 컬렉터 구현의 저자인 Mark Shannon이 진행함. 해당 GC는 프로덕션 환경에서 상당한 메모리 압박이 발생했다는 보고 이후 Python 3.13의 세대별 GC로 되돌려짐.
- 새 증분 GC의 원래 목표는 대형 힙에서 최대 일시 정지 시간을 10분의 1 수준으로 줄이는 것이었음.
- 발표 도입부에서는 Python 실행 시간이 인터프리터, 조회, 모듈, GC에 어떻게 분배되는지 그래프로 살펴봄. GC는 실행 시간의 약 11.67%를 차지함.
- JIT(Just-in-Time) 컴파일러가 실행 시간의 30.6%를 차지하는 인터프리터를 빠르게 하더라도, 메모리 관리와 GC를 개선해야 런타임을 더 빠르게 만들 수 있다는 설명임.
- Python의 주된 메모리 회수 방식은 GC가 아니라 참조 카운팅(reference counting)임. GC는 참조 카운팅을 보완하는 ‘백업’이며, 참조 카운팅이 이미 죽은 객체를 회수하므로 GC는 도달할 수 없는 순환 참조만 찾아내면 됨.
- GC의 일시 정지 시간은 서버와 사용자 인터페이스가 있는 애플리케이션에서 중요한 문제임. 증분 GC는 최대 정지 시간을 기존 세대별 GC의 약 3초에서 수십 밀리초로 낮춤.
GC 효율을 판단하는 개념
- 스캐빈지(scavenge)는 GC의 최소 작업 단위임. GC에 객체 집합을 주면 도달할 수 없는 순환 참조를 회수하며, 사용자는 내부 수행 방식을 신경 쓸 필요가 없음.
- 스캐빈지 효율(scavenge effectiveness)은 회수한 객체 수를 방문한 객체 수로 나눈 값이며, 두 Python GC의 효율은 모두 낮은 편임. 세대별 GC는 0.3%, 되돌려진 증분 GC는 약 1%임.
- 세대별 가설(generational hypothesis)은 대부분의 객체가 젊을 때 죽는다는 가정임. GC 효율을 높이는 데 활용할 수 있지만, 정확한 객체 수명 분포는 실행 프로그램에 따라 달라짐.
- 공간(space)은 연속해서 할당된 객체의 묶음임. 새 객체는 가장 최신 공간에 들어가며, 공간이 가득 차면 GC가 해당 공간을 스캐빈지할 수 있고 새 객체를 위한 빈 공간이 만들어짐. 새 객체 할당이 중단된 공간은 스캐빈지하기 좋은 대상임.
- 세대(generation)는 연속된 여러 공간의 묶음임. 세대 수는 공간 수보다 훨씬 적고 보통 GC에 미리 정해져 있으며, 공간은 세대 사이를 한 방향으로만 이동함.
- Hood Chatham은 다른 프로그래밍 언어의 GC와 Python GC의 효율을 비교할 수 있는지 질문함. Mark Shannon은 다른 언어에서는 참조 카운팅과 GC를 함께 쓰지 않으므로 직접 비교는 공정하지 않다고 답함.
증분 GC와 세대별 GC의 절충
- 되돌려진 증분 GC는 비세대별 GC로, 힙 전체를 담는 단일 세대만 사용함. 오래 존재한 공간에서 더 많은 객체가 회수되는 경향이 있어 효율적이지만, 메모리 사용량에는 부정적인 영향을 줌.
- 세대별 GC는 전반적인 메모리 사용량이 더 적지만 일시 정지 시간이 길어지는 경향이 있음. 증분 GC는 정지 시간을 줄이는 대신 메모리를 더 사용함.
- Mark Shannon은 두 방식의 장점을 결합하는 방안으로 다음 구성을 제안함.
- ‘젊은 세대’와 ‘오래된 세대’의 두 세대 구성
- 젊은 세대와 오래된 세대에서 증분 스캐빈저를 번갈아 실행
- 초기 젊은 세대 크기를 20MB로 설정
- 젊은 세대 생존율의 두 배 빈도로 오래된 세대를 스캐빈지
조정 기능과 호환성 논의
- Donghee Na는 GC 변경이 일부 사용자에게 문제를 일으킬 수 있다고 우려하며, GC 사이에 전환 기간을 둘 수 있는지 질문함. Mark Shannon은 현재 GC에 본질적인 문제가 있는 것은 아니며, 주된 부담은 유지해야 할 코드가 늘어나는 점이라고 답함. 다만 사용자가 직접 GC를 세밀하게 조정하지 않는다면 기본 GC를 개선하는 편을 선호함.
- Donghee Na는 JVM 가비지 컬렉터처럼 사용자가 설정을 조정할 수 있는 옵션을 제공할 수 있는지 질문함. Mark Shannon은 JVM 언어에서 GC가 런타임의 핵심인 것과 달리 Python에서는 참조 카운팅을 보완하는 ‘백업’이므로 그런 설정이 꼭 필요하지는 않다고 봄.
- Jukka Lehtosalo는 Mark Shannon을 돕겠다는 뜻을 밝히고, 기존 애플리케이션의 GC 세밀 조정 사례를 질문함. GC가 바뀌면 기존 조정값은 새 GC가 평균적으로 더 낫더라도 ‘낡아질’ 수 있음. Mark Shannon은 제대로 구현한다면 GC를 세밀하게 조정할 필요를 없앨 수 있다고 기대함.
- Gregory P. Smith는 GC 공개 API와 세밀 조정 기능이 일반적인 사례를 개선하는 데 제약이 된다고 지적하고, JVM처럼 여러 GC 구현을 지원하는 방식도 원하지 않는다고 밝힘. Python 3.14에서 증분 GC를 되돌린 일의 핵심 교훈은 특정 GC에는 잘 맞지만 다른 GC에는 맞지 않는 사용 사례가 있으며, 그런 사례를 테스트 스위트에 포함해야 한다는 점임.
- Tobias Wrigstad는 Java에서 채택 중인 잠재적 조정 방식으로 GC에 명시적인 CPU 예산을 제공하는 방안을 제안함. 직관적인 방식처럼 보이지만, 사용자가 GC가 죽은 메모리를 전부 회수하지 못하도록 설정할 수 있다는 부작용이 있음.
프로덕션 조정 사례와 동시 GC
- Pablo Galindo Salgado는 GC 문제를 겪은 경험을 공유하며, 프로덕션에서 GC 세대 설정을 실시간으로 조정해 즉각적인 피드백을 얻는 방식이 효과적이었다고 밝힘.
- Thomas Wouters도 ‘병적인 GC 동작’을 완화하기 위한 세밀 조정에 동의함. 이런 문제가 Python 버전을 업그레이드하는 데 흔한 장애물이라고 덧붙임. Mark Shannon은 어떤 결정을 내리기 전에 관련 수치를 살펴보자고 제안함.
- Larry Hastings는 동시 또는 ‘락 프리(lock-free)’ GC를 Python에서 구현할 가능성을 질문하며, ‘메모리를 공짜로 돌려받는다’는 점이 훌륭한 홍보 문구가 될 수 있다고 말함.
- Mark Shannon은 동시 GC 구현이 매우 어렵고, C 확장 모듈이 ‘무엇이든 할 수 있다’는 점이 락 없는 동시 GC와 충돌할 수 있다고 설명함. Tobias Wrigstad도 포인터마다 메타데이터를 넣어야 하는 등 동시 락 프리 GC가 매우 침습적인 변경이라고 동의함.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요