본문으로 건너뛰기
피드

고의 새 가비지 컬렉터 ‘그린 티’, 힙을 훑는 방식이 어떻게 달라졌나

backend 약 8분
vote
0
댓글
북마크

고 1.25에서 도입되고 1.26에서 기본값이 된 새 가비지 컬렉터 ‘그린 티’를 실제 힙 배치와 성능 측정으로 뜯어본 글이다. 핵심은 포인터를 무작정 따라가는 대신 메모리 span 단위로 훑어 캐시 친화성을 높였다는 점이고, 동시에 고의 비이동식 컬렉터가 여전히 sparse page 회수에 약하다는 한계도 같이 보여준다.

  • 1

    고 런타임은 같은 크기 클래스의 객체를 8KiB page 기반 span에 모아 배치함

  • 2

    그린 티는 포인터 추적 순서를 바꿔 랜덤 메모리 접근을 줄이고 L1 캐시 효율을 개선함

  • 3

    일반 perf의 L3 지표만 보면 개선이 잘 안 보이고, bare metal에서 L1 지표를 봐야 변화가 드러남

  • 4

    고의 GC는 객체를 이동하지 않기 때문에 90퍼센트 객체를 해제해도 메모리 단편화 때문에 회수가 제한될 수 있음

  • 5

    C#처럼 compacting GC가 있는 런타임과 비교하면 고의 설계 장단점이 꽤 선명하게 보임

고 힙을 직접 걸어보면 런타임의 성격이 보임

  • 고는 객체를 아무 데나 흩뿌리는 게 아니라, 비슷한 크기 객체끼리 꽤 질서 있게 모아 둠

    • 객체 크기는 size class로 반올림되고, 같은 size class 객체들은 하나 이상의 8KiB page로 구성된 span 안에 배치됨
    • 이 방식은 고 allocator가 영향을 받은 tcmalloc 계열에서도 흔한 size-segregated allocation 스타일임
  • 글쓴이는 작은 객체, 중간 객체, 큰 객체를 랜덤하게 할당한 뒤 힙 주소를 직접 걸어보는 실험을 함

    • 32바이트 단위로 주소 공간을 훑으면서 자기 객체를 만나면 문자로 찍는 식임
    • 랜덤하게 할당했는데도 출력 결과를 보면 같은 크기 객체가 서로 가까이 붙어 있음
  • 여기서 중요한 포인트는 GC를 돌려도 고는 객체를 옮기지 않는다는 거임

    • C# 예제와 비교하면 차이가 더 선명해짐
    • C#은 같은 크기 객체가 고처럼 딱딱 모여 있지 않고, 다른 workload에서는 객체 이동도 관찰됨

그린 티는 ‘포인터를 따라간다’는 습관을 바꿈

  • 고의 전통적인 GC는 Mark and Sweep 방식임

    • globals, locals 같은 root에서 시작해 도달 가능한 객체를 mark함
    • 이후 sweep 단계에서 mark되지 않은 객체를 죽은 객체로 보고 해제함
  • 문제는 포인터를 따라가는 방식이 캐시에 별로 안 착하다는 데 있음

    • A 객체가 B, C, D처럼 서로 다른 크기의 객체를 가리키면, 이 객체들은 고 힙의 서로 다른 영역에 있을 가능성이 큼
    • 같은 타입 A끼리 연결돼 있어도 생성 시점이 많이 다르면 메모리상으로 멀리 떨어질 수 있음
    • GC가 포인터를 만나는 대로 따라가면 결과적으로 랜덤 메모리 접근이 늘어남
  • 그린 티는 이 흐름을 span 중심으로 바꿈

    • 포인터 하나를 발견하자마자 곧장 따라가기보다, span 안의 객체와 포인터를 훑고 다음에 스캔할 span을 큐에 넣는 식임
    • 말하자면 ‘객체 그래프 산책’보다 ‘메모리 구역 단위 청소’에 가까워짐

중요

> 그린 티의 핵심은 GC 알고리즘 이름이 멋져졌다는 게 아니라, mark 단계의 메모리 접근 패턴을 캐시 친화적으로 바꿨다는 데 있음.

perf 숫자는 그냥 보면 함정이 있음

  • 글쓴이는 두 가지 workload를 만들고 기존 GC와 그린 티를 비교함

    • 하나는 포인터 접근이 비교적 잘 모인 경우고, 다른 하나는 랜덤하게 흩어진 경우임
    • 랜덤 인덱스 생성 비용이 측정에 섞이지 않도록 Python으로 미리 index 파일을 만들어 둠
  • 처음 perf 결과에서는 기대와 다르게 cache miss가 늘어난 것처럼 보임

    • perf의 cache-references와 cache-misses는 보통 L3 캐시 기준으로 해석되는 경우가 많음
    • 그런데 실제로는 프로그램 실행 시간이 이미 빨라졌고, L1이나 L2 레벨의 변화가 가려져 있었음
  • 그래서 instruction 수를 같이 보고 MPKI로 정규화했지만, VM 환경에서는 여전히 결정적인 그림이 잘 안 나옴

    • L1 이벤트를 보려면 PMU counter가 필요한데, 많은 VM이 이걸 제대로 노출하지 않음
    • 글쓴이는 결국 Vultr bare metal x86/amd64 Linux 머신으로 옮겨 L1-dcache-loads와 L1-dcache-load-misses를 직접 봄
  • bare metal에서야 기대한 결과가 나옴

    • L1 cache miss percentage는 같거나 줄어드는 수준이었지만, L1 MPKI는 더 뚜렷하게 감소함
    • 더 많은 read가 L1이나 L2에서 해결되면서 L3 cache miss가 덜 중요해진 셈임

그래도 고 GC의 오래된 약점은 남아 있음

  • 고는 non-moving collector라서 메모리를 compact하거나 defragment하지 않음

    • 이 설계는 포인터 안정성이나 런타임 단순성 측면에서는 장점이 있음
    • 대신 객체를 많이 지워도 남은 객체가 page 곳곳에 듬성듬성 박혀 있으면 page 전체를 회수하기 어려움
  • 글쓴이는 처음 힙 시각화 예제를 변형해 객체의 90퍼센트를 해제하는 실험을 함

    • 기대만 보면 메모리 사용량도 90퍼센트 가까이 줄어야 할 것 같음
    • 실제로는 고가 객체를 옮기지 않기 때문에 빈 공간이 생겨도 OS에 깔끔하게 돌려주지 못하는 경우가 생김
  • 단순한 경우에는 사용자가 직접 객체를 새 위치로 복사해 단편화를 줄일 수도 있음

    • 물론 이건 애플리케이션 코드에서 항상 쓸 수 있는 일반 해법은 아님
    • 그래도 문제의 본질이 ‘GC가 안 돈다’가 아니라 ‘살아남은 객체 배치 때문에 page가 회수되지 않는다’는 걸 잘 보여줌
  • C# 샘플에서 90퍼센트 객체를 지웠을 때는 compacting GC의 장점이 더 잘 드러남

    • 객체 이동이 가능한 런타임은 남은 객체를 모아 메모리 회수를 더 잘할 수 있음
    • 반대로 고는 낮은 지연과 단순한 런타임 모델을 얻는 대신 이런 최악 케이스를 감수하는 쪽임

기술 맥락

  • 고가 그린 티에서 바꾼 건 ‘무엇을 지울까’보다 ‘어떤 순서로 읽을까’에 가까워요. GC가 하는 일은 여전히 살아 있는 객체를 찾고 죽은 객체를 치우는 거지만, 큰 힙에서는 같은 일을 해도 메모리를 어떤 패턴으로 읽느냐가 실행 시간을 꽤 갈라요.

  • 왜 span 단위 접근이 중요하냐면 CPU 캐시는 근처 메모리를 한꺼번에 가져오는 쪽에 유리하거든요. 포인터를 따라 힙 여기저기를 뛰어다니면 L1이나 L2에서 못 잡고 더 먼 캐시나 메모리까지 내려갈 확률이 커져요.

  • 기사에서 perf 해석이 길게 나오는 이유도 이 때문이에요. L3 miss 비율만 보면 그린 티가 별로인 것처럼 보일 수 있는데, 실제 병목이 L1 쪽에서 풀렸다면 L3 숫자만으로는 결론을 내리기 어려워요.

  • 고가 여전히 객체를 옮기지 않는다는 점도 의도적인 선택이에요. 객체 이동이 없으면 런타임과 포인터 모델은 단순해지지만, 오래 살아남은 객체가 page 곳곳에 남는 workload에서는 메모리 회수가 생각보다 안 될 수 있어요.

고 GC 이야기는 보통 ‘빠르다/느리다’로 끝나는데, 이 글은 힙 주소를 직접 걸어보고 캐시 계층까지 내려가서 왜 빨라지는지 보여주는 게 맛있다. 서버 고 개발자라면 메모리 사용량이 줄지 않는 상황을 GC 탓으로만 뭉개기 전에 단편화와 object layout을 같이 봐야 함.

댓글

댓글

댓글을 불러오는 중...

backend

데카트론 독일, 웹 결제에 유럽식 계좌이체 ‘웨로’ 붙였다

데카트론 독일이 자사 웹사이트 결제 수단에 유럽 결제 이니셔티브의 웨로를 도입했다. 카드 네트워크를 거치지 않는 실시간 계좌 간 결제로 수수료를 낮추고, 멤버십과 결제를 묶어 고객 유지까지 노리는 움직임이다.

backend

SMTP를 HTTP 위에서 다시 만든다면? 기존 부품만 조립한 현대식 이메일 실험

글쓴이는 SMTP를 대체하려는 게 아니라, HTTP·WebFinger·Ed25519·HPKE·JMAP 같은 이미 검증된 기술로 이메일의 문제를 다시 설계해보는 HMTP 실험을 소개해. 핵심은 주소 형태만 user@domain으로 유지하고, 전송·발견·서명·암호화·스팸 방어·읽기 모델을 전부 현대 웹 인프라 위에 얹는 거야.

backend

버셀 실험실의 Scriptc, TypeScript를 Node 없이 네이티브 바이너리로 컴파일

Vercel Labs가 공개한 Scriptc는 일반 TypeScript 코드를 Node, V8, JavaScript 엔진 없이 작은 네이티브 실행 파일로 컴파일하는 실험적 컴파일러다. 정적으로 컴파일 가능한 코드와 동적 실행이 필요한 코드를 명확히 나누고, Node와 결과가 바이트 단위로 맞는지 800개 이상 테스트로 검증한다.

backend

SIMD는 고수 전용 흑마법이 아니라, 모든 개발자가 알아야 할 반복문 최적화임

미첼 하시모토가 SIMD를 ‘복잡한 고성능 영역’으로만 볼 필요가 없다고 설명한 글이다. Ghostty의 실제 Zig 코드를 예로 들며, 큰 배열이나 문자열을 훑는 뜨거운 반복문은 벡터 너비만큼 한 번에 처리하는 5단계 패턴으로 생각할 수 있다고 풀어낸다.

backend

스타트업이 Postgres로 버티려면 알아야 할 생존 팁

Hatchet이 2년 동안 프로덕션 Postgres를 굴리며 겪은 문제를 스키마, 쿼리, 마이그레이션, 커넥션, 쿼리 플래너, autovacuum, 파티셔닝까지 한 번에 정리한 글이다. 핵심은 “인덱스만 붙이면 끝”이 아니라, 락·통계·배치·부풀어 오른 테이블까지 운영 관점에서 같이 봐야 한다는 점이다.