Posted On 2026년 02월 15일

Go가 아무것도 안 하면서 1.5MB를 차지하는 이유

nobaksan 0 comments
여행하는 개발자 >> 기술 >> Go가 아무것도 안 하면서 1.5MB를 차지하는 이유

Go로 가장 단순한 프로그램을 만들어 보자.

package main

func main() {
}

아무것도 하지 않는 프로그램이다. 이걸 컴파일하면 놀라운 일이 벌어진다. 바이너리 크기가 1.5MB다. 같은 일을 하는 C 프로그램은 16KB에 불과한데 말이다. 왜 이런 차이가 날까?

Go는 네 코드가 실행되기 전에 엄청난 양의 준비 작업을 하기 때문이다. 그 1.5MB 안에는 메모리 할당자, 가비지 컬렉터, 스케줄러, 시스템 모니터, 그리고 고루틴과 채널과 맵을 지원하는 모든 기반 코드가 들어있다.

첫 번째 놀라운 사실. 네가 작성한 main 함수는 프로그램의 진입점이 아니다.

$ readelf -h nothing_go | grep "Entry point"
  Entry point address: 0x467280

$ go tool nm nothing_go | grep 467280
  467280 T _rt0_amd64_linux

진짜 진입점은 _rt0_amd64_linux라는 어셈블리 함수다. 이 함수는 명령행 인수를 스택에서 가져온 뒤 rt0_go로 점프한다. 여기서부터 본격적인 부트스트랩이 시작된다.

Go에서 코드는 고루틴 위에서 실행되고, 고루틴은 OS 스레드 위에서 돌아간다. 그래서 런타임이 제일 먼저 하는 일은 첫 번째 고루틴(g0)과 첫 번째 스레드(m0)를 만드는 것이다. g0는 조금 특별하다. 네 코드를 실행하지 않고 런타임의 내부 작업에만 쓰인다.

다음으로 Thread-Local Storage를 설정한다. TLS는 각 스레드가 자기만의 저장 공간을 갖게 해주는 OS 기능이다. Go는 이걸 이용해서 지금 어떤 고루틴이 실행 중인지 빠르게 알아낸다. TLS 설정이 실패하면 프로그램은 바로 종료된다. 그만큼 중요한 기능이라는 뜻이다. CPU 기능도 탐지한다. AVX나 AES-NI 같은 기능을 지원하는지 확인해서, 바이너리가 특정 명령어를 쓰도록 컴파일됐는데 CPU가 지원하지 않으면 미리 에러를 내고 종료한다.

본격적인 초기화

schedinit 함수가 호출되면 본격적인 초기화가 시작된다. 먼저 스택 풀이 만들어진다. Go 고루틴은 2KB의 작은 스택으로 시작해서 필요하면 자동으로 늘어난다. 런타임은 여러 크기의 스택 세그먼트를 미리 풀에 저장해둔다. 고루틴을 만들 때마다 OS에 메모리를 요청하면 느리니까, 미리 준비해두는 것이다.

메모리 할당자도 초기화된다. 핵심 아이디어는 간단하다. 메모리가 필요할 때마다 OS에 요청하면 느리니까, 큰 덩어리를 미리 받아서 작게 잘라 쓴다. 메모리는 68개 크기 클래스로 관리된다. 8바이트부터 32KB까지. 50바이트를 할당하면 딱 50바이트가 아니라 가장 가까운 클래스인 64바이트 슬롯을 받는다.

P라는 개념도 중요하다. P는 Processor의 약자인데, 워크스테이션이라고 생각하면 이해하기 쉽다. 고루틴이 실행되려면 P에 앉아야 하고, OS 스레드가 그 워크스테이션을 운영한다. 각 P는 자기만의 고루틴 실행 대기열과 메모리 캐시를 가지고 있다. 덕분에 대부분의 메모리 할당이 락 없이 빠르게 이루어진다. P의 개수는 GOMAXPROCS 환경변수로 결정되는데, 기본값은 CPU 코어 수다.

마지막 단계

스케줄러가 준비되면 runtime.main이라는 고루틴이 생성된다. 네 main이 아니라 런타임의 main이다. 여기서 시스템 모니터가 시작된다. sysmon이라는 백그라운드 스레드인데, 워치독 역할을 한다. 고루틴이 P를 너무 오래 독점하면 양보하게 만들고, 시스템 콜에 묶인 스레드에서 P를 빼앗아 다른 고루틴에게 주고, 가비지 컬렉터가 한동안 안 돌았으면 실행시킨다.

네 패키지와 임포트한 모든 패키지의 init 함수가 의존성 순서대로 실행된다. 그리고 드디어, 네가 작성한 main 함수가 호출된다.

main 함수가 반환되면 런타임은 바로 종료하지 않는다. 패닉을 처리 중인 고루틴이 정리할 시간을 잠깐 준다. 하지만 아직 돌고 있는 다른 고루틴들은 경고 없이 그냥 죽는다. 끝까지 기다려야 한다면 sync.WaitGroup 같은 것으로 명시적으로 동기화해야 한다.

결국 Go가 아무것도 안 하면서 느린 게 아니다. 사실 엄청나게 많은 일을 한다. 고루틴이 가벼운 건 스택 풀과 할당자가 이미 준비되어 있기 때문이고, 메모리 관리가 투명한 건 GC가 이미 돌고 있기 때문이고, 동시성이 그냥 작동하는 건 스케줄러가 첫 줄 코드 실행 전에 이미 세팅됐기 때문이다. 1.5MB는 비용이 아니라 투자다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Related Post

왜 학생들은 컴퓨터공학을 떠나 AI로 향하는가

올 가을, 미국 UC 캠퍼스에서 예상치 못한 일이 벌어졌다. 닷컴 버블 붕괴 이후 처음으로 컴퓨터공학…

웹어셈블리(WebAssembly)가 서버에서 떠오르고 있다

WebAssembly는 브라우저를 위해 만들어졌지만 이제 서버에서 더 주목받고 있다. Fermyon, Fastly, Cloudflare가 WebAssembly 런타임을 엣지…

PostgreSQL이 데이터베이스 세계를 집어삼키고 있다

카네기 멜론 대학의 Andy Pavlo 교수가 2025년 데이터베이스 회고를 발표했다. 그의 결론은 명확하다. PostgreSQL의 지배가…