CS:APP 9장 가상메모리 & Malloc C언어로 구현

C언어로 직접 Malloc을 구현해보자

말록 구현에 앞서 명시적 할당자(Explicit allocator)묵시적 할당자(Implicit allocator) 를 구분해두자. 둘의 차이는 free를 누가, 직접/간접 중 어느 방식으로 수행하느냐에 있다.

구분 Explicit allocator Implicit allocator
free 방식 프로그래머가 직접 호출 시스템이 간접적으로 회수
사용 예 C의 malloc / free Java의 garbage collection, ML, Lisp

malloc의 기본 개념은 이전 포스팅 동적 메모리 할당(Dynamic memory allocation)을 참조하면 된다. 이 글에서는 Explicit allocator인 malloc을 직접 구현한다.

1. Malloc 할당 개요

말록 할당

위 사진의 할당/해제 순서는 다음과 같다.

순서 동작
1 p1 할당 (int × 4)
2 p2 할당 (int × 5)
3 p3 할당 (int × 6)
4 p2 free
5 p4 할당 (int × 2)

응용 프로그램의 자유

  • 응용 프로그램은 mallocfree를 마음대로 요청할 수 있다
  • 단, free는 반드시 malloc된 블록에 대해서만 가능하다

Allocator의 제약

명시적 할당자는 다음의 엄격한 제한 안에서 동작해야 한다.

  • 할당된 블록의 개수·크기를 관리할 권한이 없다
  • malloc 요청에 즉시 응답해야 한다 (나중으로 미룰 수 없다)
  • free된 메모리에서만 할당할 수 있다
  • 정렬 요구사항을 지켜야 한다 (리눅스 기준 x86: 8byte, x86-64: 16byte)
  • free 블록만 조작할 수 있다
  • 한 번 할당된 블록은 이동할 수 없다 (= 압축 불가)

2. 핵심 용어

블록 용어 정리

용어 설명
Throughput 시간당 완료된 요청 수 (5000 malloc + 5000 free를 10초에 끝내면 1000 ops/sec)
Payload 블록 안에서 실제로 사용하는 데이터
Overhead 블록 안에서 payload를 제외한 나머지 (header, footer, padding)
Aggregate payload 할당된 블록들의 데이터 총합
Peak utilization 할당기가 힙을 얼마나 효율적으로 쓰는지를 나타내는 지표 (높을수록 좋음)

최고 이용도는 다음과 같이 정의한다. $P_i$는 시점 $i$까지의 할당된 데이터 합, $H_k$는 현재 힙 크기다.

\[U_k = \frac{\max_{i \le k} P_i}{H_k}\]

단편화 두 종류

종류 원인 설명
내부 단편화 (Internal) overhead, padding 블록 내부 공간을 효율적으로 못 쓰는 것
외부 단편화 (External) 가용 블록의 분산 가용 블록 총합은 충분하지만, 적당한 크기의 연속 블록이 없는 것

외부 단편화

3. 블록 설계 기초

3-1. Header의 탄생

포인터로 블록과 블록을 건너뛰려면 “payload가 어디서 시작하고 어디까지 읽어야 하는가”를 알아야 한다. 그래서 header라는 overhead에 블록 크기를 저장하자는 아이디어가 나왔다.

  • header 덕분에 각 블록의 시작점을 순차적으로 탐색할 수 있게 됐다
  • 책에서는 payload 시작 주소를 기준으로, 그 앞에 overhead(header) 를 두기로 했다
  • 그런데 태초의 첫 주소는 앞에 공간이 없어 header를 못 넣는다 → 그래서 첫 주소에는 항상 padding을 넣어준다

헤더

추가로, 블록이 가용인지 비가용인지도 구분해야 한다. 이미 할당된 블록에 또 할당하면 안 되기 때문이다.

정렬(32bit→8byte, 64bit→16byte) 때문에 블록 크기의 하위 3~4bit는 항상 0으로 남는다. 이 남는 비트를 활용하자.

  • overhead를 읽을 때 하위 3~4bit를 제외하고 읽으면 블록 크기, 그 비트들만 따로 읽으면 부가 정보가 된다
  • 책에서는 32bit 시스템(3bit 남음)을 사용해, 가장 첫 번째 비트에 가용/비가용(1/0) 을 저장한다

헤더

3-2. Footer의 탄생

할당과 free를 반복하면 free 블록이 연속될 수 있다. 아래 사진처럼 4-size와 2-size 가용 블록이 붙어 있는데 5-size를 할당하려 하면, “할당 크기보다 같거나 큰 블록을 찾는” 코드 특성상 할당에 실패한다(외부 단편화).

연속된 가용 블럭

이 코드를 유지하면서 해결하는 방향은 다음과 같다.

  1. 연속된 가용 블록이 생기지 않게 한다 — 연속된 가용 블록이 없으면 아쉬울 일이 없다
  2. free 시 앞뒤 가용 블록을 병합(coalesce)한다
    • 뒤 블록은 현재 header 크기를 더해 위치를 알 수 있다
    • 앞 블록은 크기를 모른다 → footer를 도입해 블록 뒤에 크기·가용 정보를 저장한다
  3. (extra) footer도 overhead라 없는 게 좋다. 모든 블록에 footer가 필요할까?
    • 남는 3~4bit에 앞 블록의 가용/비가용 정보를 함께 넣으면, footer는 가용 블록에만 필요하고 할당된 블록에는 불필요해진다

4. 묵시적 리스트 (Implicit List)

4-1. First-fit

CS:APP 교재 기반의 묵시적 리스트 + first-fit 구현이다.

/*
 * mm-naive.c - The fastest, least memory-efficient malloc package.
 *
 * In this naive approach, a block is allocated by simply incrementing
 * the brk pointer.  A block is pure payload. There are no headers or
 * footers.  Blocks are never coalesced or reused. Realloc is
 * implemented directly using mm_malloc and mm_free.
 *
 * NOTE TO STUDENTS: Replace this header comment with your own header
 * comment that gives a high level description of your solution.
 */
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>

#include "mm.h"
#include "memlib.h"

team_t team = {
    "ateam",
    "Harry Bovik",
    "bovik@cs.cmu.edu",
    "",
    ""};

/* single word (4) or double word (8) alignment */
#define ALIGNMENT 8
#define WSIZE 4 /* header/footer size */
#define DSIZE 8 /* double word size */
#define CHUNKSIZE (1 << 12) /* Extend heap by this amount (bytes) */

#define MAX(x, y) ((x) > (y) ? (x) : (y))

/* Pack a size and allocated bit into a word */
#define PACK(size, alloc) ((size) | (alloc))

/* Read and write a word at address p */
#define GET(p) (*(unsigned int *)(p))
#define PUT(p, val) (*(unsigned int *)(p) = (val))

/* Read the size and allocated fields from address p */
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)

/* Given block ptr bp, compute address of its header and footer */
#define HDRP(bp) ((char *)(bp) - WSIZE)
#define FTRP(bp) ((char *)(bp) + GET_SIZE(HDRP(bp)) - DSIZE)

/* Given block ptr bp, compute address of next and previous blocks */
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(((char *)(bp)-WSIZE)))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE(((char *)(bp)-DSIZE)))

/* rounds up to the nearest multiple of ALIGNMENT */
#define ALIGN(size) (((size) + (ALIGNMENT - 1)) & ~0x7)

#define SIZE_T_SIZE (ALIGN(sizeof(size_t)))

static char *heap_listp;
int mm_init(void);
static void *extend_heap(size_t words);
static void *find_fit(size_t asize);
static void place(void *bp, size_t asize);
static void *coalesce(void *bp);

int mm_init(void)
{
    /* Create the initial empty heap */
    if ((heap_listp = mem_sbrk(4*WSIZE)) == (void *)-1)
        return -1;
    PUT(heap_listp, 0);
    PUT(heap_listp + (1*WSIZE), PACK(DSIZE, 1));
    PUT(heap_listp + (2*WSIZE), PACK(DSIZE, 1));
    PUT(heap_listp + (3*WSIZE), PACK(0, 1));
    heap_listp += (2*WSIZE);

    /* Extend the empty heap with a free block of CHUNKSIZE bytes */
    if (extend_heap(CHUNKSIZE/WSIZE) == NULL)
        return -1;
    return 0;
}

static void *extend_heap(size_t words)
{
    char *bp;
    size_t size;

    /* Allocate an even number of words to maintain alignment */
    size = (words % 2) ? (words + 1) * WSIZE : words * WSIZE;
    if ((long)(bp = mem_sbrk(size)) == -1)
        return NULL;

    /* Initialize free block header/footer and the epilogue header */
    PUT(HDRP(bp), PACK(size, 0)); /* Free block header */
    PUT(FTRP(bp), PACK(size, 0)); /* Free block footer */
    PUT(HDRP(NEXT_BLKP(bp)), PACK(0, 1)); /* New epilogue header */

    /* Coalesce if the previous block was free */
    return coalesce(bp);
}

void *mm_malloc(size_t size)
{
    size_t asize;
    size_t extendsize;
    char * bp;

    /* Ignore spurious requests */
    if (size == 0)
        return NULL;

    /* Adjust block size to include overhead and alignment reqs. */
    if (size <= DSIZE)
        asize = 2 * DSIZE;
    else
        asize = DSIZE * ((size + DSIZE + (DSIZE - 1)) / DSIZE);

    /* Search the free list for a fit */
    if ((bp = find_fit(asize)) != NULL)
    {
        place(bp, asize);
        return bp;
    }

    /* No fit found. Get more memory and place the block */
    extendsize = MAX(asize, CHUNKSIZE);
    if ((bp = extend_heap(extendsize/WSIZE)) == NULL)
        return NULL;
    place(bp, asize);
    return bp;
}

static void *find_fit(size_t asize)
{
    /* First-fit search */
    void *bp;

    for (bp = heap_listp; GET_SIZE(HDRP(bp)) > 0; bp = NEXT_BLKP(bp))
    {
        if (!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp))))
        {
            return bp;
        }
    }
    return NULL;
}

static void place(void *bp, size_t asize)
{
    size_t csize = GET_SIZE(HDRP(bp));

    if ((csize - asize) >= (2*DSIZE))
    {
        PUT(HDRP(bp), PACK(asize, 1));
        PUT(FTRP(bp), PACK(asize, 1));
        bp = NEXT_BLKP(bp);
        PUT(HDRP(bp), PACK(csize-asize, 0));
        PUT(FTRP(bp), PACK(csize-asize, 0));
    }
    else
    {
        PUT(HDRP(bp), PACK(csize, 1));
        PUT(FTRP(bp), PACK(csize, 1));
    }
}

void mm_free(void *ptr)
{
    size_t size = GET_SIZE(HDRP(ptr));

    PUT(HDRP(ptr), PACK(size, 0)); /* 헤더를 미할당 상태로 update */
    PUT(FTRP(ptr), PACK(size, 0)); /* 푸터를 미할당 상태로 update */
    coalesce(ptr); /* free 끼리 병합 */
}

static void *coalesce(void *bp)
{
    size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp)));
    size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
    size_t size = GET_SIZE(HDRP(bp));

    if (prev_alloc && next_alloc)
        return bp;

    else if (prev_alloc && !next_alloc)
    {
        size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
        PUT(HDRP(bp), PACK(size, 0));
        PUT(FTRP(bp), PACK(size, 0));
    }
    else if (!prev_alloc && next_alloc)
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp)));
        PUT(FTRP(bp), PACK(size, 0));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }
    else
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp)));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }
    return bp;
}

void *mm_realloc(void *ptr, size_t size)
{
    void *oldptr = ptr;
    void *newptr;
    size_t copySize;

    newptr = mm_malloc(size);
    if (newptr == NULL)
        return NULL;
    copySize = *(size_t *)((char *)oldptr - SIZE_T_SIZE);
    if (size < copySize)
        copySize = size;
    memcpy(newptr, oldptr, copySize);
    mm_free(oldptr);
    return newptr;
}

이 코드의 실행 결과는 아래와 같다.

묵시적 first-fit 결과

4-2. Next-fit

묵시적 방식에서 가장 빠르다는 next-fit을 구현해봤다. next_ptr 전역변수를 두고, 마지막으로 찾은 위치부터 탐색을 이어가는 방식이다. 바뀐 부분만 정리했다.

static void *next_ptr = NULL; // 전역변수로 선언

static void *extend_heap(size_t words)
{
    char *bp;
    size_t size;

    size = (words % 2) ? (words + 1) * WSIZE : words * WSIZE;
    if ((long)(bp = mem_sbrk(size)) == -1)
        return NULL;

    PUT(HDRP(bp), PACK(size, 0));
    PUT(FTRP(bp), PACK(size, 0));
    PUT(HDRP(NEXT_BLKP(bp)), PACK(0, 1));
    next_ptr = bp; /* Set next_ptr to the new block */

    return coalesce(bp);
}

static void *find_fit(size_t asize)
{
    void *bp;

    for (bp = next_ptr; GET_SIZE(HDRP(bp)) > 0; bp = NEXT_BLKP(bp))
    {
        if (!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp))))
        {
            next_ptr = bp;
            return bp;
        }
    }
    return NULL;
}

static void *coalesce(void *bp)
{
    size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp)));
    size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
    size_t size = GET_SIZE(HDRP(bp));

    if (prev_alloc && next_alloc)
        return bp;
    else if (prev_alloc && !next_alloc)
    {
        size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
        PUT(HDRP(bp), PACK(size, 0));
        PUT(FTRP(bp), PACK(size, 0));
    }
    else if (!prev_alloc && next_alloc)
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp)));
        PUT(FTRP(bp), PACK(size, 0));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }
    else
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp)));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }
    next_ptr = bp;
    return bp;
}

coalesce에서 free한 블록에 next_ptr을 넣는 것은 엄밀히는 next-fit 개념에 어긋나지만, 테스트 케이스가 많은 편이어서 이렇게 진행했다. 결과적으로 점수가 21점 올랐다.

묵시적 next-fit 결과

5. 명시적 리스트 (Explicit List)

마지막으로 명시적 리스트를 구현했다. 가용 블록끼리 이중 연결 리스트로 묶어 find_fit이 free 블록만 순회하도록 한 방식이다.

핵심은 payload 공간에 다음/이전 free 블록을 가리키는 포인터를 저장하는 매크로다.

  • NEXT_FREE(bp): bp를 이중 포인터로 형변환해 역참조 → bp가 가리키는 메모리에 저장된 “다음 free 블록 포인터”를 읽고 쓴다
  • PREV_FREE(bp): 같은 원리지만 bp + PTR_SIZE 위치에 “이전 free 블록 포인터”를 저장한다
  • SET_NEXT_FREE / SET_PREV_FREE: 위 위치에 실제 주소값을 써넣는다
/* (생략된 헤더 주석/include/team_t는 위와 동일) */

/* single word (4) or double word (8) alignment */
#define ALIGNMENT 16
#define WSIZE 4 /* header/footer size */
#define PTR_SIZE sizeof(void *) /* pointer size */
#define MIN_FREE_BLOCK_SIZE (WSIZE + PTR_SIZE + PTR_SIZE + WSIZE)
#define CHUNKSIZE (1 << 12)

#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define PACK(size, alloc) ((size) | (alloc))
#define GET(p) (*(unsigned int *)(p))
#define PUT(p, val) (*(unsigned int *)(p) = (val))
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)
#define HDRP(bp) ((char *)(bp) - WSIZE)
#define FTRP(bp) ((char *)(bp) + GET_SIZE(HDRP(bp)) - (WSIZE*2))
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(((char *)(bp)-WSIZE)))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE(((char *)(bp)-(WSIZE*2))))

/* free list 포인터 매크로 (payload 공간에 next/prev free 포인터 저장) */
#define NEXT_FREE(bp) (*(void **)(bp))
#define PREV_FREE(bp) (*(void **)((char *)(bp) + PTR_SIZE))
#define SET_NEXT_FREE(bp, ptr) (NEXT_FREE(bp) = (ptr))
#define SET_PREV_FREE(bp, ptr) (PREV_FREE(bp) = (ptr))

#define ALIGN(size) (((size) + (ALIGNMENT - 1)) & ~0xf)
#define SIZE_T_SIZE (ALIGN(sizeof(size_t)))

static void *free_root = NULL; /* Pointer to the first free block */
static char *heap_listp;

static void add_to_free_list(void *bp);
static void remove_from_free_list(void *bp);
int mm_init(void);
static void *extend_heap(size_t words);
static void *find_fit(size_t asize);
static void place(void *bp, size_t asize);
static void *coalesce(void *bp);

int mm_init(void)
{
    if ((heap_listp = mem_sbrk(4*WSIZE)) == (void *)-1)
        return -1;
    PUT(heap_listp, 0);
    PUT(heap_listp + (1*WSIZE), PACK((WSIZE*2), 1));
    PUT(heap_listp + (2*WSIZE), PACK((WSIZE*2), 1));
    PUT(heap_listp + (3*WSIZE), PACK(0, 1));
    heap_listp += (2*WSIZE);
    free_root = NULL;

    if (extend_heap(CHUNKSIZE/WSIZE) == NULL)
        return -1;
    return 0;
}

static void *extend_heap(size_t words)
{
    char *bp;
    size_t size;

    size = (words * WSIZE); // words는 WSIZE 단위
    size = ALIGN(size);     // 최종 크기를 16의 배수로 올림

    if ((long)(bp = mem_sbrk(size)) == -1)
        return NULL;

    PUT(HDRP(bp), PACK(size, 0));
    PUT(FTRP(bp), PACK(size, 0));
    PUT(HDRP(NEXT_BLKP(bp)), PACK(0, 1));

    return coalesce(bp); // free_root 설정/포인터 초기화는 coalesce에서 처리
}

void *mm_malloc(size_t size)
{
    size_t asize;
    size_t extendsize;
    char * bp;

    if (size == 0)
        return NULL;

    size_t needed = size + (WSIZE * 2);

    if (needed <= MIN_FREE_BLOCK_SIZE)
        asize = MIN_FREE_BLOCK_SIZE;
    else
        asize = ALIGN(needed);

    if ((bp = find_fit(asize)) != NULL)
    {
        place(bp, asize);
        return bp;
    }

    extendsize = MAX(asize, CHUNKSIZE);
    if ((bp = extend_heap(extendsize/WSIZE)) == NULL)
        return NULL;
    place(bp, asize);
    return bp;
}

static void *find_fit(size_t asize)
{
    /* First-fit search (free list만 순회) */
    void *bp;

    for (bp = free_root; bp != NULL; bp = NEXT_FREE(bp))
    {
        if (asize <= GET_SIZE(HDRP(bp))) // 크기만 비교
        {
            return bp;
        }
    }
    return NULL;
}

static void place(void *bp, size_t asize)
{
    size_t csize = GET_SIZE(HDRP(bp));

    remove_from_free_list(bp); // 먼저 free list에서 제거

    if ((csize - asize) >= (MIN_FREE_BLOCK_SIZE)) // 분할 가능
    {
        PUT(HDRP(bp), PACK(asize, 1));
        PUT(FTRP(bp), PACK(asize, 1));

        void *next_bp = NEXT_BLKP(bp);
        PUT(HDRP(next_bp), PACK(csize - asize, 0));
        PUT(FTRP(next_bp), PACK(csize - asize, 0));

        add_to_free_list(next_bp); // 남은 free 블록 다시 추가
    }
    else // 분할 불가 (전체 사용)
    {
        PUT(HDRP(bp), PACK(csize, 1));
        PUT(FTRP(bp), PACK(csize, 1));
    }
}

void mm_free(void *ptr)
{
    size_t size = GET_SIZE(HDRP(ptr));

    PUT(HDRP(ptr), PACK(size, 0));
    PUT(FTRP(ptr), PACK(size, 0));
    coalesce(ptr);
}

static void *coalesce(void *bp)
{
    void *prev_blk = PREV_BLKP(bp);
    void *next_blk = NEXT_BLKP(bp);

    size_t prev_alloc = GET_ALLOC(FTRP(prev_blk));
    size_t next_alloc = GET_ALLOC(HDRP(next_blk));
    size_t size = GET_SIZE(HDRP(bp));

    // Case 1: 병합 불필요
    if (prev_alloc && next_alloc) {
        add_to_free_list(bp);
        return bp;
    }
    // Case 2: 다음 블록과 병합
    else if (prev_alloc && !next_alloc) {
        remove_from_free_list(next_blk);
        size += GET_SIZE(HDRP(next_blk));
        PUT(HDRP(bp), PACK(size, 0));
        PUT(FTRP(bp), PACK(size, 0));
        add_to_free_list(bp);
        return bp;
    }
    // Case 3: 이전 블록과 병합
    else if (!prev_alloc && next_alloc) {
        remove_from_free_list(prev_blk);
        size += GET_SIZE(HDRP(prev_blk));
        bp = prev_blk;
        PUT(HDRP(bp), PACK(size, 0));
        PUT(FTRP(bp), PACK(size, 0));
        add_to_free_list(bp);
        return bp;
    }
    // Case 4: 양쪽 모두 병합
    else {
        remove_from_free_list(prev_blk);
        remove_from_free_list(next_blk);
        size += GET_SIZE(HDRP(prev_blk)) + GET_SIZE(HDRP(next_blk));
        bp = prev_blk;
        PUT(HDRP(bp), PACK(size, 0));
        PUT(FTRP(bp), PACK(size, 0));
        add_to_free_list(bp);
        return bp;
    }
}

void *mm_realloc(void *ptr, size_t size)
{
    void *oldptr = ptr;
    void *newptr;
    size_t copySize;

    newptr = mm_malloc(size);
    if (newptr == NULL)
        return NULL;
    copySize = GET_SIZE(HDRP(oldptr)) - (WSIZE*2);
    if (size < copySize)
        copySize = size;
    memcpy(newptr, oldptr, copySize);
    mm_free(oldptr);
    return newptr;
}

// --- Helper 함수 ---
// Free list 맨 앞에 블록 추가 (LIFO)
static void add_to_free_list(void *bp) {
    if (free_root == NULL) {
        SET_NEXT_FREE(bp, NULL);
        SET_PREV_FREE(bp, NULL);
        free_root = bp;
    } else {
        SET_NEXT_FREE(bp, free_root);
        SET_PREV_FREE(bp, NULL);
        SET_PREV_FREE(free_root, bp);
        free_root = bp;
    }
}

// Free list에서 블록 제거
static void remove_from_free_list(void *bp) {
    void *prev_free = PREV_FREE(bp);
    void *next_free = NEXT_FREE(bp);

    if (prev_free == NULL) { // bp가 첫 번째 블록
        free_root = next_free;
    } else {
        SET_NEXT_FREE(prev_free, next_free);
    }

    if (next_free != NULL) { // bp가 마지막 블록이 아님
        SET_PREV_FREE(next_free, prev_free);
    }
}

결과는 1점이 더 올랐다. 명시적 리스트에서는 메모리 효율 면에서 best-fit이 가장 좋다.

명시적 first-fit 결과

6. 명시적 리스트 Refactoring

처음엔 명시적 리스트의 포인터 변경을 coalesce 안에서 직접 다 처리했는데, 이게 꽤 복잡했다. 그 개선 과정을 정리한다.

명시적 리스트는 가용 블록의 포인터 생성/변경을 고려해야 하는데, 특히 양쪽 병합인 Case 4는 포인터를 8개나 다뤄야 한다.

Case 4

그래서 초기 코드는 아래처럼 case마다 포인터를 일일이 갱신하는 형태로 비대해졌다.

static void *coalesce(void *bp)
{
    size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp)));
    size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
    size_t size = GET_SIZE(HDRP(bp));
    void *prev_free_root;
    void *prev_free_root_prev;
    void *next_free_blk_next;
    void *next_free_blk_prev;
    void *prev_free_blk_next;
    void *prev_free_blk_prev;

    if (prev_alloc && next_alloc)               // Case 1
    {
        if (free_root == NULL)
        {
            free_root = bp;
            SET_NEXT_FREE(bp, NULL);
            SET_PREV_FREE(bp, NULL);
        }
        else
        {
            prev_free_root = free_root;
            SET_PREV_FREE(prev_free_root, bp);
            SET_NEXT_FREE(bp, prev_free_root);
            SET_PREV_FREE(bp, NULL);
            free_root = bp;
        }
    }
    else if (prev_alloc && !next_alloc)         // Case 2
    {
        size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
        PUT(HDRP(bp), PACK(size, 0));
        PUT(FTRP(bp), PACK(size, 0));

        if (free_root == NULL)
        {
            free_root = bp;
            SET_NEXT_FREE(bp, NULL);
            SET_PREV_FREE(bp, NULL);
        }
        else
        {
            prev_free_root = free_root;
            prev_free_root_prev = PREV_FREE(free_root);
            next_free_blk_next = NEXT_FREE(NEXT_BLKP(bp));
            next_free_blk_prev = PREV_FREE(NEXT_BLKP(bp));
            SET_NEXT_FREE(next_free_blk_prev, next_free_blk_next);
            SET_PREV_FREE(next_free_blk_next, next_free_blk_prev);
            SET_NEXT_FREE(bp, prev_free_root);
            SET_PREV_FREE(bp, NULL);
            SET_PREV_FREE(prev_free_root_prev, bp);
            free_root = bp;
        }
    }
    else if (!prev_alloc && next_alloc)         // Case 3
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp)));
        PUT(FTRP(bp), PACK(size, 0));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);

        if (free_root == NULL)
        {
            free_root = bp;
            SET_NEXT_FREE(bp, NULL);
            SET_PREV_FREE(bp, NULL);
        }
        else
        {
            prev_free_root = free_root;
            prev_free_root_prev = PREV_FREE(free_root);
            prev_free_blk_next = NEXT_FREE(PREV_BLKP(bp));
            prev_free_blk_prev = PREV_FREE(PREV_BLKP(bp));
            SET_NEXT_FREE(prev_free_blk_prev, prev_free_blk_next);
            SET_PREV_FREE(prev_free_blk_next, prev_free_blk_prev);
            SET_NEXT_FREE(bp, prev_free_root);
            SET_PREV_FREE(bp, NULL);
            SET_PREV_FREE(prev_free_root_prev, bp);
            free_root = bp;
        }
    }
    else                                        // Case 4
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp)));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);

        prev_free_root = free_root;
        prev_free_root_prev = PREV_FREE(free_root);
        prev_free_blk_next = NEXT_FREE(PREV_BLKP(bp));
        prev_free_blk_prev = PREV_FREE(PREV_BLKP(bp));
        next_free_blk_next = NEXT_FREE(NEXT_BLKP(bp));
        next_free_blk_prev = PREV_FREE(NEXT_BLKP(bp));
        SET_NEXT_FREE(prev_free_blk_prev, prev_free_blk_next);
        SET_PREV_FREE(prev_free_blk_next, prev_free_blk_prev);
        SET_NEXT_FREE(next_free_blk_prev, next_free_blk_next);
        SET_PREV_FREE(next_free_blk_next, next_free_blk_prev);
        SET_NEXT_FREE(bp, prev_free_root);
        SET_PREV_FREE(bp, NULL);
        SET_PREV_FREE(prev_free_root_prev, bp);
        free_root = bp;
    }
    free_root = bp;
    return bp;
}

포인터 조작을 두 개의 헬퍼 함수로 분리했다.

Google AI Studio

6-1. 분리한 헬퍼 함수

// Free list 맨 앞에 블록 추가 (LIFO)
static void add_to_free_list(void *bp) {
    if (free_root == NULL) {
        SET_NEXT_FREE(bp, NULL);
        SET_PREV_FREE(bp, NULL);
        free_root = bp;
    } else {
        SET_NEXT_FREE(bp, free_root);   // 현재 블록의 next를 기존 첫 free 블록으로
        SET_PREV_FREE(bp, NULL);        // 현재 블록의 prev는 NULL
        SET_PREV_FREE(free_root, bp);   // 기존 첫 free 블록의 prev를 현재 블록으로
        free_root = bp;
    }
}

// Free list에서 블록 제거
static void remove_from_free_list(void *bp) {
    void *prev_free = PREV_FREE(bp);
    void *next_free = NEXT_FREE(bp);

    if (prev_free == NULL) { // bp가 첫 번째 블록
        free_root = next_free;
    } else {
        SET_NEXT_FREE(prev_free, next_free);
    }

    if (next_free != NULL) { // bp가 마지막 블록이 아님
        SET_PREV_FREE(next_free, prev_free);
    }
}

각 함수가 담당하는 포인터를 사진 번호로 매핑하면 다음과 같다.

함수 담당 포인터
add_to_free_list ①, ②, ③, ④
remove_from_free_list ⑤, ⑥, ⑦, ⑧

Case4 함수별 포인터

6-2. 개선된 Case 4

덕분에 Case 4가 다음처럼 간결해졌다. 복잡한 포인터 갱신은 모두 헬퍼 함수가 담당한다.

// Case 4: 양쪽 모두 병합
else { // (!prev_alloc && !next_alloc)
    remove_from_free_list(prev_blk); // 이전 블록 제거
    remove_from_free_list(next_blk); // 다음 블록 제거
    size += GET_SIZE(HDRP(prev_blk)) + GET_SIZE(HDRP(next_blk));
    bp = prev_blk; // 병합된 블록의 시작으로 이동
    PUT(HDRP(bp), PACK(size, 0));
    PUT(FTRP(bp), PACK(size, 0));
    add_to_free_list(bp); // 최종 병합 블록을 리스트에 추가
    return bp;
}

© 2022 JeongHwan Yun.