본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Computer Science

메모리 풀

작성 2026. 10. 14.추가
목차7개 항목
출처와 참고

팔만코딩경(42Seoul 커뮤니티 위키)의 「메모리 풀, malloc()보다 100배 빠르다!」를 바탕으로 재구성한 노트다.

메모리 풀이란

메모리 풀(memory pool)은 큰 메모리 덩어리를 한 번에 확보해 두고, 할당 요청이 오면 그 안에서 미리 정한 크기의 칸을 떼어 주는 할당 방식이다. node를 자주 만들고 지운다면 node[1000]만큼 한 번에 잡아 두고 한 칸씩 빌려주는 식이다.

왜 malloc이 느린가

malloc/free(그리고 그 위의 new/delete)는 어떤 크기의 요청이든 처리해야 하는 범용 할당기다.

비용내용
빈 공간 탐색크기별 free list나 bin에서 맞는 블록을 찾는다
분할과 병합큰 블록을 쪼개 주고, 해제된 이웃 블록을 다시 합친다
메타데이터블록마다 크기·상태 헤더를 둔다
스레드 동기화여러 스레드가 같은 힙을 쓰면 잠금이나 스레드별 캐시가 필요하다
시스템 콜힙이 모자랄 때만 brk/mmap으로 OS에 요청한다. 매 호출이 시스템 콜은 아니다

크기가 하나로 정해진 풀은 탐색·분할·병합이 필요 없어 이 비용 대부분이 사라진다. 할당 패턴과 크기에 따라 크게 빨라지며, 같은 종류의 객체가 연속 메모리에 모여 캐시 지역성도 좋아진다.

고정 크기 풀

storage:     [ 0 ][ 1 ][ 2 ][ 3 ][ 4 ] ...   ← 같은 크기 칸들
free_index:  [ 4, 3, 2, 1, 0 ]  top = 5       ← 비어 있는 칸 번호 스택
 
alloc(): return &storage[free_index[--top]]
free(p): free_index[top++] = (p - storage)

빈 칸 번호를 스택에 쌓아 두면 할당과 해제가 각각 한 줄, O(1)이다. 빈 칸 자체에 다음 빈 칸의 주소를 적어 두는 free list 방식을 쓰면 별도 인덱스 배열도 필요 없다.

template <typename T, size_t N>
class FixedPool {
    alignas(T) unsigned char storage[N * sizeof(T)];
    size_t freeIndex[N];
    size_t top = N;
public:
    FixedPool() { for (size_t i = 0; i < N; ++i) freeIndex[i] = N - 1 - i; }
    void* allocate()        { assert(top > 0); return storage + freeIndex[--top] * sizeof(T); }
    void  deallocate(void* p) {
        freeIndex[top++] = (static_cast<unsigned char*>(p) - storage) / sizeof(T);
    }
};
  • assert는 디버그 빌드에서만 검사하고 릴리스 빌드에서는 사라지므로 정상 경로 비용이 없다.
  • 이 풀은 생 메모리만 돌려준다. 객체는 placement new(new (p) T(...))로 생성하고, 반납 전에 소멸자를 직접 불러야 한다.

가변 풀: 고정 풀을 청크로 잇기

고정 풀의 한계는 용량이 처음에 정해진다는 것이다. 최대 사용량을 아는 프로그램이면 충분하지만, 모르면 풀이 꽉 찬 순간 할당이 실패한다.

flowchart TB
    vp["VariablePool"] --> c0["chunk 0: FixedPool<br>(가득 참)"]
    vp --> c1["chunk 1: FixedPool<br>(빈 칸 있음) ← 여기서 할당"]
    vp --> c2["모두 가득 차면 chunk 2를<br>새로 malloc해 목록에 추가"]

블록 하나의 구조:

flowchart LR
    hdr["chunk 번호<br/>← 해제 시 앞쪽 헤더를 읽어 어느 청크로 돌려줄지 안다"] ~~~ data["데이터"]

각 블록 앞에 출신 청크 번호를 붙여 두면 free할 때 주소 몇 바이트 앞만 읽어 올바른 청크로 돌려보낼 수 있다.

operator new/delete 오버로딩

풀을 직접 호출하게 하면 실수하기 쉽다. 클래스에 operator new/operator delete를 정의하면 평소처럼 new/delete만 써도 그 클래스는 풀을 쓴다.

struct MsgBlock {
    char msg[512];
    static void* operator new(size_t)    { return pool.allocate(); }
    static void  operator delete(void* p) { pool.deallocate(p); }
private:
    static FixedPool<MsgBlock, 4096> pool;
};
 
MsgBlock* m = new MsgBlock;   // 풀에서 할당
delete m;                     // 풀로 반납

클래스마다 전용 힙을 두면 다른 스레드와 잠금을 나눌 일이 없고, 메모리 누수나 손상이 어느 클래스에서 났는지 추적하기도 쉽다.

쓰임새와 주의점

쓰임새이유
네트워크 서버의 메시지 버퍼같은 크기 버퍼가 끊임없이 생기고 사라진다. 버퍼를 연속 페이지에 모으면 잠금(pinning)할 페이지 수도 줄어든다
게임의 총알·파티클프레임마다 생성·파괴가 반복된다. 객체 자체를 재사용하는 오브젝트 풀 패턴도 같은 발상이다
파서·컴파일러의 AST 노드한꺼번에 만들고 한꺼번에 버리는 아레나(arena) 할당이 잘 맞는다
  • 풀은 크기와 수명이 비슷한 객체에만 이득이다. 크기가 제각각이면 범용 할당기가 낫다.
  • 풀을 여러 스레드가 공유하면 잠금이 다시 필요하다. 스레드별 풀을 두는 편이 단순하다.
  • 범용 할당기도 발전했다. glibc의 스레드별 캐시(tcache), jemalloc·mimalloc 같은 할당기가 작은 객체 할당을 이미 빠르게 처리하므로, 풀을 만들기 전에 프로파일러로 할당이 병목인지 확인한다.
  • C++17의 std::pmr(polymorphic memory resource)은 monotonic_buffer_resource, unsynchronized_pool_resource 같은 풀을 표준으로 제공해 컨테이너에 바로 꽂아 쓸 수 있다.

정리

  • 메모리 풀은 큰 블록을 미리 잡아 두고 고정 크기 칸을 빌려주는 방식이다.
  • 범용 할당기의 탐색·분할·병합·잠금 비용을 없애 할당·해제를 O(1)로 만든다.
  • 용량이 고정되는 한계는 고정 풀을 청크로 잇는 가변 풀로 푼다.
  • operator new/delete 오버로딩이나 std::pmr로 호출 코드를 바꾸지 않고 풀을 붙인다.
  • 만들기 전에 할당이 정말 병목인지 측정한다.

관련 노트: 포인터와 메모리, 메모리 관리, C++ STL 컨테이너와 시간 복잡도

원본 기록2

댓글

GitHub 계정으로 댓글을 남길 수 있어요.