메모리 풀: 고정 크기 블록 할당

EmbeddedMemory

메모리 풀은 동일한 크기의 블록을 미리 할당해두고, 요청 시 블록 단위로 꺼내 쓰는 할당 방식이다. malloc의 단편화, 비결정적 실행 시간 문제를 해결한다.

구조

메모리 풀 (블록 크기: 64B, 총 8개)
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ blk 0  │ blk 1  │ blk 2  │ blk 3  │ blk 4  │ blk 5  │ blk 6  │ blk 7  │
│ [used] │ [used] │ [free] │ [free] │ [used] │ [free] │ [free] │ [free] │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
                     ↓         ↓                  ↓         ↓        ↓
                   free list: 2 → 3 → 5 → 6 → 7 → NULL
  • 모든 블록이 같은 크기이므로 외부 단편화가 발생하지 않는다
  • free list에서 head를 꺼내면 되므로 할당/해제가 O(1)이다
  • 블록당 메타데이터(size header)가 필요 없어 메모리 오버헤드가 적다

구현

#define BLOCK_SIZE  64
#define BLOCK_COUNT 16

typedef struct {
    uint8_t memory[BLOCK_COUNT][BLOCK_SIZE];
    uint8_t *free_list[BLOCK_COUNT];
    size_t free_count;
} MemPool;

void mempool_init(MemPool *pool) {
    pool->free_count = BLOCK_COUNT;
    for (size_t i = 0; i < BLOCK_COUNT; i++) {
        pool->free_list[i] = pool->memory[i];
    }
}

void *mempool_alloc(MemPool *pool) {
    if (pool->free_count == 0) return NULL;
    return pool->free_list[--pool->free_count];
}

void mempool_free(MemPool *pool, void *ptr) {
    assert(pool->free_count < BLOCK_COUNT);
    pool->free_list[pool->free_count++] = (uint8_t *)ptr;
}

free list를 스택처럼 운용한다. alloc은 pop, free는 push. 루프나 탐색이 없으므로 실행 시간이 일정하다.

연결 리스트 방식

블록 자체에 다음 포인터를 저장하면 별도 배열 없이 free list를 구성할 수 있다. 미사용 블록의 메모리 공간을 포인터 저장에 활용한다.

typedef struct MemPool {
    uint8_t memory[BLOCK_COUNT * BLOCK_SIZE];
    void *free_head;
    size_t block_size;
} MemPool;

void mempool_init(MemPool *pool) {
    pool->block_size = BLOCK_SIZE;
    pool->free_head = pool->memory;
    // 각 블록의 첫 부분에 다음 블록 주소를 저장
    for (size_t i = 0; i < BLOCK_COUNT - 1; i++) {
        void **block = (void **)(pool->memory + i * BLOCK_SIZE);
        *block = pool->memory + (i + 1) * BLOCK_SIZE;
    }
    void **last = (void **)(pool->memory + (BLOCK_COUNT - 1) * BLOCK_SIZE);
    *last = NULL;
}

void *mempool_alloc(MemPool *pool) {
    if (pool->free_head == NULL) return NULL;
    void *block = pool->free_head;
    pool->free_head = *(void **)block;
    return block;
}

void mempool_free(MemPool *pool, void *ptr) {
    *(void **)ptr = pool->free_head;
    pool->free_head = ptr;
}

별도 free list 배열이 필요 없어 메모리를 더 절약한다. 단, 블록 크기가 포인터 크기(sizeof(void *)) 이상이어야 한다.

다중 크기 풀

요청 크기가 다양할 때는 크기별 풀을 여러 개 두고 라우팅한다.

static MemPool pool_32;   // 32B 블록
static MemPool pool_128;  // 128B 블록
static MemPool pool_512;  // 512B 블록

void *sized_alloc(size_t size) {
    if (size <= 32)  return mempool_alloc(&pool_32);
    if (size <= 128) return mempool_alloc(&pool_128);
    if (size <= 512) return mempool_alloc(&pool_512);
    return NULL;  // 지원하지 않는 크기
}

요청 크기보다 큰 블록에 할당되므로 내부 단편화가 발생한다. 풀 크기 설계 시 실제 할당 패턴을 프로파일링해서 블록 크기와 개수를 결정해야 한다.

RTOS 통합

FreeRTOS는 heap_2.c에서 고정 크기 블록 풀과 유사한 방식을 사용한다. 멀티태스크 환경에서는 alloc/free를 critical section으로 보호해야 한다.

void *mempool_alloc_safe(MemPool *pool) {
    taskENTER_CRITICAL();
    void *ptr = mempool_alloc(pool);
    taskEXIT_CRITICAL();
    return ptr;
}

메모

  • malloc 대비 약 7배 빠르다는 벤치마크 결과가 있다 (Windows 환경, CodeProject 참고)
  • 블록 크기를 2의 거듭제곱으로 설정하면 정렬 문제가 자연스럽게 해결된다
  • 풀 소진 시 동작 정책(NULL 반환, assert, 대기)을 시스템 요구사항에 맞게 결정해야 한다
  • double free 방지가 필요하면 블록 상태 비트맵을 추가하거나, 해제 시 주소 범위 검증을 넣는다