Užijte si šálek zeleného čaje, který Vám ukradne Go runtime, GreenTea GC
왜 또 새로운 GC가 나오나
기존의 GC는 왜
현재 Go의 GC는 다음과 같은 말로 설명할 수 있습니다.
- concurrent tri-color mark and sweep
- 총 3가지 색상(하양, 회색, 검정)을 사용하여 객체의 상태를 추적합니다.
- 하양: 아직 방문하지 않은 객체
- 회색: 방문했지만 자식 객체를 모두 방문하지 않은 객체
- 검정: 모든 자식 객체를 방문한 객체
- 순회가 끝나고 하얀색 객체는 모두 수집됩니다.
- Write Barrier를 사용하여 GC 도중 새로 생성되는 메모리를 보호합니다.
- Write Barrier를 On/Off하는 시간이 흔히 말하는 Stop-The-World(이하 STW)의 상당 부분입니다.
- 이 과정을 동시적으로 수행합니다.
- 총 3가지 색상(하양, 회색, 검정)을 사용하여 객체의 상태를 추적합니다.
- No Compaction
- Go의 GC는 컴팩션을 하지 않습니다.
- 메모리 파편화가 발생할 수 있습니다.
- 하지만 32kb이하의 객체는 고 언어의 메모리 할당자(Allocator)의 per-P 캐시를 활용하여 파편화를 최소화합니다.
- Non-Generational
- Go의 GC는 세대별로 객체를 관리하지 않습니다.
- 모든 객체는 동일한 세대에 속합니다.
- Escape Analysis
- Go는 Escape Analysis를 통해 객체가 힙에 할당되는지 스택에 할당되는지를 결정합니다.
- 대략적으로 댕글링 포인터나 인터페이스를 사용한다면 힙에 할당된다고 볼 수 있습니다.
중요한 건
Go의 GC는 모든 객체를 루트로부터 탐색해 들어가서 삼색 마크를 수행한다는 점입니다. 이 과정을 한줄로 적으면 위상 정렬 그래프 동시 탐색 알고리즘
이라고 할 수 있습니다. 하지만 각 객체는 서로다른 메모리 영역에 존재할 가능성이 높죠. 단적으로 얘기해서,
- 서로 32MB 정도 떨어진 거리에 있는 메모리 영역이 있다고 합시다.
- 이 두 메모리 영역에 각 5개의 객체가 할당되어 있습니다.
- 객체 A, B, C는 1번 영역에
- 객체 D, E는 2번 영역에
- 객체의 참조 순서는
A -> D -> B -> E -> C
입니다. - 이제 GC가 시작되면, A를 방문하고 D를 방문하고 B를 방문하고 E를 방문하고 C를 방문합니다.
- 이때, A와 D는 서로 다른 메모리 영역에 존재하기 때문에, A를 방문하고 D를 방문하는 과정에서 메모리 영역을 이동해야 합니다.
- 이 과정에서는 메모리 영역을 이동하고 새로운 캐시 라인을 채우는 등, 소위 메모리 점프 비용이 발생합니다. 애플리케이션이 사용하는 CPU 시간의 상당 부분이 이러한 GC 작업에 할당될 수 있습니다.
- 이 비용은 GC가 진행되는 동안 계속 발생합니다.
그래서 뭐가 문제임?
이러한 GC의 동작은 다음 경우에서 쥐약입니다.
- 코어 수가 많고, 메모리가 클 경우
- Go의 GC는 기본적으로 동시적으로 수행됩니다.
- 하지만 메모리 영역이 넓고 객체가 분산되어 있다면, 여러 코어가 동시적으로 다양한 메모리 공간에서 객체를 탐색하게 됩니다.
- 이 과정에서 CPU와 메모리 사이의 버스가 충분히 크지 않는 다면, 메모리 대역폭이 병목이 될 수 있습니다.
- 또한 실제 물리적으로 거리가 있기에 탐색 한번한번이 비교적 큰 딜레이가 될 것입니다.
- 자잘한 객체가 많은 경우
- 만약 여러분들이 뎁스가 깊거나, 자식이 많은 트리나 그래프를 고로 운영한다면, GC는 이 객체들을 모두 탐색해야 합니다.
- 이 과정에서 객체가 서로 다른 메모리 영역에 분산되어 있다면, 첫번째 이유로 인해 딜레이가 발생합니다.
- 또한, 자잘한 객체가 많다면, GC는 이 객체들을 모두 탐색해야 하기에, GC가 수행되는 동안 CPU 코어가 상당히 많은 역량을 GC에 할당하고 일을 하게 될 것입니다.
이러한 문제를 해결하기 위해 Golang 팀은 GreenTea GC를 발표했습니다.
당신에게 더 이상의 녹차를 마실 여유는 없다
무엇이 녹차를 빼앗는가
기존 GC의 가장 빠른 해결책을 적용할 수 있는 영역으로 본 것은 메모리 지역성으로 보입니다. 즉, 객체가 서로 가까이 위치하도록 하여 메모리 점프를 최소화하는 것입니다. 하지만 코드를 작성하는 프로그래머의 패턴을 강제할 수 없고, 또 워크플로우에 따라 객체 할당은 예측할 수 없습니다.
그래서인지 Golang 팀이 선택한 방법은 **메모리 스팬(Memory Span)**입니다.
메모리 스팬이란?
메모리 스팬은 비교적 큰 메모리 영역을 할당 받아서 작은 객체를 할당하는 곳입니다. 그리고 GreenTea GC라고 이름 지어진 새로운 GC는 이 메모리 스팬에 대해 쓰레기 수집을 수행합니다. 세부 동작은 기존 Tri-Color Mark and Sweep와 거의 동일합니다.
GreenTea GC의 동작
먼저 GreenTea GC는 메모리 스팬을 할당합니다. 크기는 미리 서술했던 대로, 어느정도 크기가 있는 8KiB입니다. 그리고 이 안에는 최대 512 bytes 크기의 객체를 할당할 수 있습니다. 딱 예시를 들었던 트리나 그래프의 노드 크기, 혹은 일반적인 힙으로 탈주하는 구조체의 크기가 이거보다 크긴 어렵다 싶은 정도의 크기입니다. 512 bytes 이하의 객체는 힙으로 탈주할 때마다 이 메모리 스팬에 쌓이고, 이 메모리 스팬이 가득 차면 새로운 메모리 스팬을 할당합니다.
이제 GC가 발생할 때에 GreenTea GC는 이 메모리 스팬을 큐에 쌓아 순차적으로 검사합니다. 이 과정에서 GreenTea GC는 기존의 GMP 모델과 거의 유사한 스케쥴링을 사용합니다. 작업 강탈같은 영역 또한 구현되어 있습니다. 여튼 큐에서 메모리 스팬을 꺼낸 워커는 자신에게 할당된 메모리 스팬의 내부 객체를 검사합니다. 이 과정에서 Tri-Color Mark and Sweep이 동일하게 사용됩니다.
이게 무슨 이점이?
이 과정에서 기존 GC와의 차이는 크게 보면 딱 하나입니다. 한번에 GC를 수행하는 단위가 객체에서 메모리 스팬이 되었다는 점입니다. 이로 인해 GC는 다음과 같은 이점을 가집니다.
- 메모리 지역성: 객체가 메모리 스팬에 모여 있기 때문에, 객체를 탐색할 때 메모리 점프가 최소화됩니다. 즉, CPU 캐시를 최대한 활용할 수 있습니다.
- GC 성능 향상: 메모리 스팬 단위로 GC를 수행함으로써, GC가 멀티 코어 환경에서 더 효율적으로 작동합니다.
- 메모리 할당 최적화: 메모리 스팬은 고정 크기로 할당되므로, 작은 객체를 할당할 때 메모리 할당의 오버헤드가 줄어들고, 파편화 발생 가능성이 감소합니다. 이는 메모리 할당 해제의 효율성을 높입니다.
짧게 말하면, 이제부터 저희는 더 자주, 그리고 가벼운 마음으로 512 bytes 이하의 객체를 할당할 수 있다는 점입니다.
다만 여기에도 어느 정도 유리한 구간이 있습니다.
- 트리/그래프 구조: fan-out이 높고 잘 변경이 일어나지 않는 경우
- B+ 트리, Trie, AST (Abstract Syntax Tree)
- 캐시 친화적인 데이터 구조
- 배치 처리: 작은 데이터의 대량 할당 워크플로우
- JSON 파싱으로 생성되는 다수의 작은 객체
- 데이터베이스 결과셋의 DAO 객체들
이러한 경우에 GreenTea GC는 기존 GC보다 더 나은 성능을 발휘할 수 있습니다. 하지만 모든 경우에 적용되는 것은 아니며, 특히 객체가 서로 다른 메모리 영역에 분산되어 있는 경우에는 여전히 드라마틱한 성능 향상을 기대하기는 어렵습니다.
그래서
Golang 팀은 아마 장기적으로 GC를 계속 개선해나갈 생각이 있어 보입니다. 이번 GreenTea GC는 작은 영역에서의 마이너 체인지라고 볼 수 있고, 기존 GC를 대체하는 것은 아닙니다. 하지만 GreenTea GC는 Golang 팀이 직면한, 혹은 예상하는 문제를 엿볼 수 있는 재밌는 사례로 보입니다. 의외로 복잡한 문제지만, 비교적 간단한 개념의 추가로 해결하려는 시도도 인상 깊었습니다. 개인적으로 재밌는 사례라고 생각합니다.
GreenTea GC는 Go 1.25부터 도입되는 실험실 기능입니다. GOEXPERIMENT=greenteagc
환경 변수를 사용함으로 활성화하여 사용할 수 있습니다. 이 기능은 아직 실험적이므로, 프로덕션 환경에서 사용하기 전에 충분한 테스트가 필요합니다.