C++ 프로그램이 시작되자마자 72KB를 할당하는 이유: Emergency Pool과 메모리 관리의 미학
최근 Hacker News에서 꽤 흥미로운 글을 하나 발견했습니다. “Why is the first C++ (m)allocation always 72 KB?”라는 제목의 글이었는데, 평소 시스템 프로그래밍이나 메모리 할당자(Allocator)를 직접 건드려본 엔지니어라면 한 번쯤 고개를 갸웃했을 법한 주제입니다.
오늘은 이 글을 바탕으로 C++ 표준 라이브러리(libstdc++)가 숨기고 있는 Emergency Pool 의 정체와, 왜 우리가 이 72KB라는 숫자를 마주하게 되는지, 그리고 이것이 현대 시스템 프로그래밍에 시사하는 바가 무엇인지 깊이 파헤쳐 보겠습니다.
미스터리의 시작: 72KB의 정체
커스텀 메모리 할당자를 작성하거나 LD_PRELOAD를 이용해 malloc을 후킹(hooking) 해본 경험이 있다면, C++ 프로그램이 시작되자마자 무조건적으로 발생하는 73,728 bytes (72KB) 크기의 할당 요청을 목격했을 겁니다. Hello World만 찍어도, 아무것도 안 하고 종료해도 이 할당은 발생합니다.
원문 작성자인 Joel Siks는 자신의 커스텀 할당자를 테스트하다가 이 패턴을 발견했습니다. gdb와 LD_PRELOAD를 이용해 백트레이스(Backtrace)를 추적한 결과, 범인은 바로 libstdc++였습니다.
범인은 libstdc++의 예외 처리(Exception Handling)
결론부터 말하자면, 이 메모리는 C++의 Exception Handling(예외 처리) 인프라를 위해 미리 확보된 Emergency Pool 입니다.
여기서 의문이 생깁니다. “예외 처리를 하는데 왜 굳이 힙(Heap) 메모리를, 그것도 프로그램 시작 시점에 미리 할당하는가?”
이 질문에 대한 답은 C++의 설계 철학인 Safety 와 관련이 깊습니다. C++에서 메모리 할당(malloc 또는 new)이 실패하면 무슨 일이 벌어질까요? 표준에 따르면 std::bad_alloc 예외를 던져야 합니다. 그런데 여기서 모순이 발생합니다.
“메모리가 부족해서 예외를 던져야 하는데, 예외 객체를 생성하고 던지는 과정 자체에도 메모리가 필요하다면?”
이것은 닭이 먼저냐 달걀이 먼저냐 하는 문제와 비슷합니다. 시스템 메모리가 고갈된 상황(OOM)에서 std::bad_alloc을 던지려는데, 예외 처리를 위한 메모리조차 할당받지 못한다면 프로그램은 그대로 terminate()를 호출하며 비정상 종료될 것입니다. 이는 C++가 지향하는 ‘우아한 실패(Graceful Failure)‘가 아닙니다.
Emergency Pool의 메커니즘
libstdc++는 이 문제를 해결하기 위해 프로그램 시작 시(정확히는 예외 처리 인프라가 초기화될 때) Emergency Pool 이라는 비상금 통장을 만듭니다. 평소에는 일반적인 malloc을 통해 예외 객체를 할당하지만, malloc이 실패하면 이 비상금 통장(Emergency Pool)을 털어서 예외를 처리합니다.
왜 하필 72KB인가?
이 크기는 libstdc++ 소스 코드(eh_alloc.cc)에 정의된 휴리스틱에 의해 결정됩니다. 64비트 시스템 기준으로 대략적인 계산식은 다음과 같습니다.
- 객체 수:
4 * sizeof(void*) * sizeof(void*)= 4 * 8 * 8 = 256개 (기본값) - 객체 크기: 예외 객체 헤더 등을 포함한 크기
이 값들을 계산해보면 약 72KB가 나옵니다. 물론 이 값은 고정된 것이 아니라 GLIBCXX_TUNABLES 환경 변수를 통해 조절할 수 있습니다. 만약 임베디드 환경처럼 메모리가 극도로 제한된 상황이라면, 이 풀의 크기를 줄이거나 아예 정적 버퍼(Static Buffer)를 사용하도록 빌드 옵션을 조정할 수도 있습니다.
Valgrind와 메모리 누수 오해
이 72KB 할당은 종종 주니어 엔지니어들을 혼란에 빠뜨립니다. Valgrind 같은 도구로 메모리 릭(Leak)을 검사할 때, 이 영역이 “Still Reachable” 혹은 누수로 보고되는 경우가 있기 때문입니다.
과거 버전의 Valgrind나 특정 환경에서는 프로그램이 종료될 때 이 Emergency Pool이 명시적으로 free되지 않아서 경고가 뜨곤 했습니다. 엄밀히 말하면 운영체제가 프로세스 종료 시 페이지를 회수하므로 실제 누수는 아니지만, “Clean Exit”을 지향하는 개발자에게는 눈엣가시죠.
다행히 최신 libstdc++와 Valgrind는 __gnu_cxx::__freeres 같은 훅을 통해 종료 시점에 이 메모리를 정리하도록 개선되었습니다. 하지만 여전히 레거시 시스템에서는 이 72KB가 “유령 메모리”처럼 보일 수 있다는 점을 기억해야 합니다.
Principal Engineer’s View: 설계의 우아함 vs 비용
저는 이 메커니즘을 보며 “시스템 프로그래밍의 낭만” 과 “현실적인 타협” 을 동시에 느낍니다.
- 안전망의 비용: 72KB는 현대 서버 환경에서는 티끌 같은 용량입니다. 하지만 이 작은 비용을 지불함으로써, C++는 “메모리가 바닥난 최악의 상황에서도 최소한의 유언(Exception)은 남길 수 있다”는 보장을 제공합니다. 이것이 엔터프라이즈급 언어의 품격입니다.
- 숨겨진 동작의 위험성: 반면, 이러한 “암시적 할당(Implicit Allocation)“은 투명성을 해칩니다. 사용자가 명시하지 않은 메모리가 라이브러리 단에서 자동으로 할당되는 동작은, 리소스 제약이 심한 환경(임베디드, 실시간 시스템)에서는 치명적인 지연(Latency)이나 OOM을 유발하는 트리거가 될 수도 있습니다.
우리가 배워야 할 점
많은 개발자들이 new와 delete를 단순히 언어의 문법으로만 받아들입니다. 하지만 그 이면에는 OS의 메모리 관리자, C 런타임(glibc), 그리고 C++ 표준 라이브러리의 예외 처리기까지 복잡하게 얽힌 레이어가 존재합니다.
이 72KB의 미스터리는 단순히 “메모리를 좀 먹는다”는 사실을 넘어, “극한의 상황에서도 신뢰성을 보장하기 위해 시스템이 어떤 희생을 치르고 있는가” 를 보여주는 훌륭한 사례입니다.
여러분의 서버가 OOM으로 죽어가면서도 마지막 로그 한 줄을 남길 수 있었던 건, 어쩌면 프로그램 시작부터 묵묵히 자리를 지키고 있었던 이 72KB의 Emergency Pool 덕분일지도 모릅니다.