segregated free list로 바꾸며 달라진 것
2편 explicit allocator 위에 크기 클래스 배열을 얹으면서 어떤 함수와 탐색 흐름이 바뀌는지를 코드 중심으로 따라가는 3편이다.
왜 3편의 핵심은 free list를 더 많이 만든다보다 “탐색 시작점을 나눈다”에 있나
2편 explicit free list는 힙 전체를 훑는 implicit 구조보다 훨씬 나았지만, 여전히 free block 전부가 한 체인 안에 있었다. 그래서 큰 블록과 작은 블록이 같은 리스트 안에 섞여 있고, find_fit는 매번 그 체인을 처음부터 더듬어야 했다. 이번 구현은 그 체인을 버리는 게 아니라 크기별로 나눈 여러 head 배열로 바꾸면서, 탐색을 더 작은 후보 집합에서 시작하게 만든다. 즉 3편의 본질은 “free list가 여러 개다”보다 어느 리스트에서 탐색을 시작할지를 결정하는 정책이 코드에 들어온다는 데 있다.
mm.c 안의 allocator를 구현하고, mdriver가 그 네 함수(mm_init, mm_malloc, mm_free, mm_realloc)를 trace로 검증하는 실습이다.3편은 allocator 전체를 새로 쓰는 단계가 아니다. 2편 explicit 구조를 최대한 재사용하면서, free block이 들어가는 입구와 탐색 시작점을 크기 클래스별로 분기하는 단계다.
바뀐 핵심 아이디어를 한 줄로 말하면
2편 explicit free list가 “free block만 따로 순회한다”였다면, 3편 segregated free list는 “free block도 크기가 비슷한 것끼리 먼저 모아 둔 다음, 요청 크기에 가까운 bucket부터 찾는다”로 한 단계 더 나아간다. 그래서 3편에서 제일 중요한 함수는 find_fit 하나이고, 그 find_fit를 가능하게 만드는 전처리가 get_list_index, insert_free_block, remove_free_block다.
get_list_index()는 정교한 수동 구간표가 아니라, 비트 쉬프트 기반의 거친 로그형 분류다. 그래서 이 그림은 “버킷 경계값”보다 “요청 크기에 따라 시작 index가 달라진다”는 흐름을 보여주는 용도다.3편에서 실제로 바뀐 코드는 이 다섯 군데다
free_listp가 사라지고 seg_free_lists[LISTLIMIT]가 생긴다.get_list_index()가 새로 생긴다insert_free_block()과 remove_free_block()가 bucket-aware가 된다mm_init()이 class head 전체를 초기화한다find_fit()이 “요청 크기와 가까운 class”부터 올라간다place()와 coalesce()는 인터페이스를 재사용하면서 내부 의미만 달라진다.1. free_listp 하나에서 seg_free_lists[] 배열로 넘어간다
2편 explicit allocator의 전제는 단순했다. free block이면 전부 한 연결 리스트 안에 넣고, 거기서 first-fit을 한다. 3편은 이 전제를 바꾼다. free block을 저장하는 구조는 그대로 linked list이지만, 그 linked list head가 하나가 아니라 여러 개가 된다.
static void *free_listp;
#define LISTLIMIT 10
static void *seg_free_lists[LISTLIMIT];
static int get_list_index(size_t size) {
int index = 0;
while ((index < LISTLIMIT - 1) && (size > 1)) {
size >>= 1;
index++;
}
return index;
}
2. get_list_index()는 탐색 비용을 줄이기 위한 입구 선택 함수다
이 함수는 alloc/free 자체를 하진 않지만, 3편에서 가장 중요한 정책 함수다. 같은 free block 체인이라도 asize가 24인지 512인지에 따라 처음부터 같은 bucket을 뒤질 필요는 없다. 현재 구현은 비트 쉬프트를 이용해 거칠게 크기 클래스를 나눈다.
static int get_list_index(size_t size) {
int index = 0;
while ((index < LISTLIMIT - 1) && (size > 1)) {
size >>= 1;
index++;
}
return index;
}
3. insert/remove는 이제 연결 함수가 아니라 bucket 선택까지 책임진다
2편에서 insert_free_block과 remove_free_block의 역할은 free list 하나를 LIFO 방식으로 관리하는 것이었다. 3편에서는 이 인터페이스 이름은 그대로지만, 내부 책임이 넓어진다. 이제는 “어디에 연결할지”가 아니라 “어느 리스트에 연결할지”까지 같이 결정해야 한다.
PRED(bp) = NULL;
SUCC(bp) = free_listp;
if (free_listp != NULL)
PRED(free_listp) = bp;
free_listp = bp;
int index = get_list_index(GET_SIZE(HDRP(bp)));
void *head = seg_free_lists[index];
PRED(bp) = NULL;
SUCC(bp) = head;
if (head != NULL)
PRED(head) = bp;
seg_free_lists[index] = bp;
if (PRED(bp) != NULL)
SUCC(PRED(bp)) = SUCC(bp);
else
free_listp = SUCC(bp);
int index = get_list_index(GET_SIZE(HDRP(bp)));
if (PRED(bp) != NULL)
SUCC(PRED(bp)) = SUCC(bp);
else
seg_free_lists[index] = SUCC(bp);
4. mm_init은 head 하나를 비우는 함수에서 배열 전체를 준비하는 함수가 된다
2편에서는 free list head가 하나였으니 free_listp = NULL이면 끝났다. 3편에서는 allocator가 여러 class를 동시에 관리하므로 초기화도 달라진다. 여기서 중요한 건 코드 길이가 아니라, 초기화 대상이 단일 포인터에서 여러 head 배열로 늘어난다는 점이다.
free_listp = NULL;
int i;
for (i = 0; i < LISTLIMIT; i++)
seg_free_lists[i] = NULL;
5. 진짜 성능 의도는 find_fit에서 드러난다
3편에서 가장 큰 의미를 갖는 코드는 find_fit다. 2편 explicit에서는 free block만 순회하긴 했지만, 여전히 첫 head에서 끝까지 더듬는 구조였다. 3편 segregated에서는 요청 크기와 가까운 bucket을 먼저 고르고, 거기서 맞는 블록이 없을 때만 더 큰 bucket으로 올라간다.
for (bp = free_listp; bp != NULL; bp = SUCC(bp)) {
if (asize <= GET_SIZE(HDRP(bp))) {
return bp;
}
}
for (index = get_list_index(asize); index < LISTLIMIT; index++) {
for (bp = seg_free_lists[index]; bp != NULL; bp = SUCC(bp)) {
if (asize <= GET_SIZE(HDRP(bp))) {
return bp;
}
}
}
asize가 들어온다.get_list_index(asize)가 어느 bucket부터 보는 게 맞을지 결정한다.mm_malloc(96)을 보면asize가 계산된다.get_list_index(asize)가 중간 크기 bucket을 고른다.6. place와 coalesce는 크게 안 바뀐 것처럼 보이지만, 의미는 달라진다
3편의 장점 중 하나는 2편 explicit에서 만든 인터페이스를 거의 그대로 재사용한다는 점이다. place는 여전히 할당 전에 free block을 리스트에서 제거하고, split 후 남은 조각을 다시 넣는다. coalesce도 여전히 이웃 free block을 빼고 병합한 뒤 결과를 다시 넣는다. 겉으로는 큰 변화가 없어 보여도, 내부적으로는 이제 그 insert/remove가 전부 크기 클래스별 bucket을 건드린다.
remove_free_block(bp);
...
insert_free_block(next_bp);
remove_free_block(PREV_BLKP(bp));
remove_free_block(NEXT_BLKP(bp));
...
insert_free_block(bp);
bp를 제거할 때, 2편은 단일 free list에서 뺐다.remove_free_block(bp)가 GET_SIZE(HDRP(bp))를 읽고 그 크기의 bucket에서 뺀다.next_bp는 원래 block과 크기가 다르기 때문에, 다시 넣을 때는 아예 다른 bucket으로 들어갈 수 있다.place는 “free block 하나를 빼고 남은 조각을 다시 넣는다”는 말은 같아도, 그 조각이 다른 class head 밑으로 재배치될 수 있다는 점에서 explicit보다 의미가 커진다.remove_free_block(PREV_BLKP(bp));
remove_free_block(NEXT_BLKP(bp));
size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(HDRP(NEXT_BLKP(bp)));
PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
bp = PREV_BLKP(bp);
insert_free_block(bp);
coalesce는 단순히 헤더/풋터를 다시 쓰는 함수가 아니라, 여러 bucket에 흩어져 있던 free block을 하나의 더 큰 bucket 후보로 재분류하는 함수가 된다.place와 coalesce의 상위 흐름을 재사용할 수 있다.7. 이 구현을 allocator 일반론으로 읽으면 안 되는 부분도 있다
3편이 명확히 나아진 건 맞지만, 이걸 “segregated free list의 최종형”처럼 읽으면 곤란하다. 현재 구현은 class 경계를 매우 거칠게 잡고 있고, bucket 안에서는 여전히 first-fit이다. 또 realloc은 여전히 단순 복사 방식이라, segregated free list로 바뀌었다고 해서 allocator 전체가 완성형이 된 건 아니다.
realloc 최적화와 heap checker는 아직 별도 과제다.정리하면
1편 implicit이 “힙 전체를 순회하면서 allocator 문법을 익히는 단계”였다면, 2편 explicit은 “free block만 따로 모아 탐색 대상을 줄이는 단계”였다. 이번 3편 segregated는 그 explicit 구조를 버리지 않고, 그 free block 체인을 크기 클래스별로 나눠 탐색 시작점까지 개선하는 단계다. 그래서 이 글의 핵심은 복잡한 이론보다 free_listp가 seg_free_lists[]로 바뀌고, find_fit 앞에 get_list_index()가 들어오면서 allocator가 “어디부터 볼 것인가”를 더 똑똑하게 고른다는 점에 있다.
find_fit가 이제 어떤 bucket부터 탐색하는지 손으로 따라갈 수 있는가?place와 coalesce는 코드가 덜 바뀌어 보여도, 왜 실제 의미는 달라졌는가?Malloc Lab handout은 mm.c 하나를 제출 파일로 두고, mdriver가 trace로 동작을 검증하는 구조다. 작은 trace로 시작해 구조를 확인하려면 mdriver -V -f short1-bal.rep 같은 흐름을 같이 보는 편이 좋다.
'크래프톤 정글 > 정글에서 문제풀기' 카테고리의 다른 글
| Malloc Lab 구현기 2편 - explicit free list 전환과 코드 흐름의 변화 (0) | 2026.04.14 |
|---|---|
| Malloc Lab을 활용한 Implicit Free List 구현기 (1) | 2026.04.13 |
| 이진 탐색 트리 Q5. 두 개의 스택으로 후위 순회하기 (0) | 2026.04.09 |
| 이진 탐색 트리 Q4. 한 개의 스택으로 후위 순회하기 (0) | 2026.04.09 |
| 이진 탐색 트리 Q3. 반복문으로 전위 순회하기 (0) | 2026.04.09 |