Go 메모리 할당자, 어떻게 수천 개의 고루틴(Goroutine)을 관리할까?
Go 런타임은 OS 호출 최소화를 위해 64MB 크기의 Arena를 할당받아 관리함
mcache, mcentral, mheap의 3단계 계층 구조를 통해 락(Lock) 경합(Contention)을 해결함
Span은 특정 크기의 객체를 위한 메모리 블록으로, allocBits와 gcmarkBits를 사용해 메모리 관리 및 GC 연동
Tiny Allocator를 통해 작은 객체들을 16바이트 블록에 묶어 메모리 낭비(Memory Waste)를 줄임
Go 메모리 할당 과정: Arena, Page, Span
Go 런타임은 메모리 할당을 위해 OS로부터 Arena(아레나)를 할당받아 사용한다. 각 Arena는 64MB 크기로, 이를 Page(페이지)(8KB) 단위로 분할하여 관리한다. Page는 다시 Span(스팬)으로 묶이며, Span은 특정 크기의 객체를 저장하는 데 사용된다. 이러한 구조는 OS 호출을 최소화하고, 메모리 할당 속도를 높이기 위한 핵심 설계이다. 특히, Span(스팬)은 allocBits와 gcmarkBits를 사용하여 메모리 사용 여부와 GC(Garbage Collection) 마킹 정보를 관리한다.
3단계 메모리 할당 계층 구조: mcache, mcentral, mheap
Go 런타임은 mcache, mcentral, mheap의 3단계 계층 구조를 통해 락 경합(Lock Contention) 문제를 해결한다. 각 P(Processor)는 자신만의 mcache를 가지고 있어, 대부분의 할당은 락 없이 빠르게 처리된다. mcache가 가득 차면 mcentral에서 새로운 Span을 가져오고, mcentral은 mheap에서 Page를 할당받는다. 이러한 계층 구조는 Thread-Caching Malloc(TCMalloc)을 기반으로 하며, 고루틴(Goroutine) 간의 메모리 할당 경쟁을 최소화하여 성능을 향상시킨다.
Tiny Allocator: 작은 객체 메모리 효율 개선
Go는 작은 객체(16바이트 미만, 포인터 미포함)를 위해 Tiny Allocator(타이니 할당자)를 사용한다. 이 할당자는 여러 개의 작은 객체를 16바이트 블록에 묶어 저장함으로써 메모리 낭비(Memory Waste)를 줄인다. 예를 들어, bool, int8과 같은 작은 타입들은 Tiny Allocator를 통해 효율적으로 관리된다. 이 방식은 작은 객체를 빈번하게 할당하는 프로그램의 성능을 크게 향상시킨다.
Garbage Collector(GC)와의 긴밀한 연동
Go의 메모리 할당자는 Garbage Collector(GC)와 긴밀하게 연동되어 작동한다. GC는 gcmarkBits를 사용하여 라이브 객체를 식별하고, 할당자는 이 정보를 바탕으로 메모리를 재사용한다. 또한, Scavenger(스캐빈저)는 사용하지 않는 메모리를 OS에 반환하여 메모리 자원 효율을 높인다. 이러한 상호 작용은 메모리 관리의 효율성을 극대화하고, 전반적인 시스템 성능을 향상시키는 데 기여한다.