TECH 으로 돌아가기
TECH HACKER NEWS 오늘 7분 읽기 22 READS

Go 1.26 기본 GC가 된 그린 티, 캐시 친화성과 남은 한계

Go 1.26 기본 GC가 된 그린 티, 캐시 친화성과 남은 한계
SOURCE IMAGE · HACKER NEWS

Go 1.25에서 처음 등장한 새 가비지 컬렉터 '그린 티(Green Tea)'가 몇 달 전 나온 Go 1.26부터 기본값이 됐다. 이름은 낯설지만 바뀐 부분은 GC가 살아 있는 객체를 훑는 방식이다. 이 변화가 왜 성능에 도움이 되는지, 그리고 Go가 여전히 해결하지 못한 문제는 무엇인지 짚어 보면, 메모리에 민감한 서비스를 운영하는 실무자에게 유용한 판단 재료가 된다.

Go가 메모리를 배치하는 방식

Go 런타임은 객체 크기를 가장 가까운 '크기 클래스'로 올림한 뒤, 같은 크기 클래스의 객체들을 8KiB 페이지 한 개 이상으로 이뤄진 연속 구간(Go 용어로 span)에 모아 할당한다. 이런 크기 분리 할당은 tcmalloc 같은 malloc 구현에서 흔한 방식이고, Go의 할당기도 그 계보를 잇는다. 실제로 작고 중간이고 큰 세 종류의 객체를 무작위로 할당한 뒤 힙 주소를 따라 걸어 보면, 무작위로 만들었는데도 런타임이 같은 크기의 객체를 서로 옆에 붙여 배치한 것을 눈으로 확인할 수 있다. 여기서 중요한 특징이 드러난다. GC를 돌린 뒤에도 Go는 객체를 전혀 옮기지 않는다. 즉 Go의 컬렉터는 압축(compaction)을 하지 않는 비이동(non-moving) 방식이다. 같은 실험을 C#으로 하면 런타임이 객체를 실제로 옮기는 모습이 보인다는 점에서 대조적이다.

그린 티가 바꾼 것: 포인터 추적에서 구간 스캔으로

기존 Go GC는 전역·지역 변수 같은 루트에서 출발해 포인터를 하나씩 따라가며 도달 가능한 객체를 모두 방문하는 마크 단계를 거치고, 이후 방문되지 않은 객체를 죽은 것으로 보고 해제하는 스윕 단계를 밟는다. 문제는 마크 단계의 접근 패턴이다. 객체 A가 크기가 제각각인 B·C·D를 가리키면 이들은 서로 다른 메모리 구역에 흩어져 있고, 같은 크기라도 생성 시점이 크게 다르면 역시 멀리 떨어져 있다. 포인터를 보이는 대로 따라가면 이 흩어진 위치를 무작위로 오가게 되고, 이는 측정 가능한 수준으로 캐시 친화성을 떨어뜨린다. 그린 티는 접근 순서를 뒤집는다. 포인터를 발견하는 즉시 쫓아가는 대신, 하나의 메모리 구간을 통째로 스캔해 그 안의 객체와 포인터를 찾고, 발견한 포인터를 근거로 앞으로 스캔할 구간을 큐에 쌓아 나간다. 결과적으로 접근이 구간 단위로 뭉치면서 지역성이 좋아진다.

캐시 개선은 어디서 보이나

흥미로운 점은 이 개선을 측정으로 증명하는 과정 자체가 까다롭다는 것이다. perf로 흩어진 워크로드와 새 GC를 함께 측정하면 프로그램은 분명히 빨라지는데, 정작 캐시 미스는 늘어난 것처럼 나온다. 이유는 두 가지다. 첫째, perf의 cache-references와 cache-misses는 보통 L3 캐시를 가리키므로 L1·L2 수준의 거동은 드러나지 않는다. 둘째, 새 GC로 실행 시간이 크게 달라졌는데 이를 정규화하지 않았다. 명령어 수로 나눈 지표인 MPKI(천 명령어당 미스)로 다시 계산해도 L3 MPKI는 오히려 올라간다. 결정적 단서는 L1에 있는데, 대다수 가상머신은 L1 이벤트에 필요한 PMU 카운터를 노출하지 않아 베어메탈 x86 리눅스가 필요하다. 실제로 베어메탈 장비에서 L1-dcache 지표를 잡아 계산하니 비로소 기대한 그림이 나온다. L1 MPKI가 뚜렷하게 감소한다. 더 많은 읽기가 L1(그리고 아마 L2)에서 해결돼 L3까지 내려갈 필요가 없어졌고, 그래서 L3 미스는 상대적으로 덜 중요해진 것이다. 그린 티가 실제로 개선한 지점이 여기서 구체적으로 확인된다.

비이동 컬렉터가 여전히 안고 있는 문제

한편 Go가 절대 메모리를 옮기지 않는다는 사실은 뿌리 깊은 약점을 남긴다. 앞의 S/M/L 할당 프로그램에서 객체를 만든 뒤 90%를 해제하면 메모리 사용량도 90% 가까이 줄기를 기대하지만 실제로는 그렇게 되지 않는다. 압축이나 이동이 없으니 한 span 안에 살아남은 소수의 객체가 페이지를 붙들고 있으면, 대부분이 비어 있어도 그 페이지를 온전히 회수할 수 없다. 이것이 이른바 희소 페이지(sparse page) 문제다. 다만 단순한 상황에서는 개발자가 직접 객체를 새로 복사해 한곳에 모으는 방식으로 파편화를 피하고 메모리를 되찾을 수 있다. 반면 C#처럼 객체를 옮기는 컬렉터에서 90%를 정리하면 런타임이 남은 객체를 다져 넣어 메모리를 회수한다. 그린 티가 마크 단계의 캐시 거동을 개선한 것은 분명한 진전이지만, 조각난 힙에서 유휴 메모리를 되돌려받는 문제는 GC 알고리즘의 세대 교체가 아니라 비이동 설계 자체에서 비롯된 것이라 이번 변경으로 사라지지 않는다.

한국의 실무 관점에서 정리하면, Go 1.26으로 올리는 것만으로 포인터가 많은 그래프형 워크로드는 별도 튜닝 없이 캐시 지역성 이득을 볼 여지가 있다. 다만 그 이득을 벤치마크로 확인하려면 L3 수치나 가상머신 환경에 속지 말고 명령어 정규화(MPKI)와 베어메탈 L1 측정까지 내려가야 한다. 그리고 오래 살아남는 소수 객체 때문에 힙이 부풀어 반환되지 않는 상황이라면, GC 버전 상승이 아니라 자료구조 재배치나 객체 재할당 같은 애플리케이션 차원의 대응이 여전히 필요하다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://theconsensus.dev/p/2026/07/19/observing-gos-garbage-...
SHARE
처리 중...