Explicit Allocator
explicit 방법은 할당이 해제되어있는 블록들을 연결리스트 형태로 관리하여 모든 블록을 검사하지 않고, 할당이 해제된 블록들만 검사하는 implicit에서 개서된 방법이다. 하지만 implicit에 next-fit 방식의 성능과는 유사하다.
연결리스트에 블록을 추가하는 방식에 따라 블록의 물리적인 순서대로 연결리스트가 구성되는 ordered-list 방식과 최신에 해제된 블록을 연결리스트의 가장 앞에 추가하는 LIFO 방식으로 나뉜다. 성능적인 측면에서는 LIFO 방식이, 단편화를 방지하기 위해서는 ordered-list 방식이 좋다고 한다. 아래 과정은 LIFO 방식으로 설명한다. LIFO 방식은 물리적 순서와 연결리스트의 순서가 같음을 보장하지 않음에 주의해야 한다.
implicti 방식에서 달라진 점은 가장 처음 블록에 header와 footer 이외에 prev & next block pointer가 들어간다는 점이다. 해당 정보들을 이용하여 해제된 블록들을 연결리스트의 형태로 관리한다. 가장 앞 블록은 할당이 되어있는 블록으로 연결리스트에 들어가 있어 연결리스트의 끝점을 나타내는 역할을 수행한다.
Explicit Allocator 메모리 할당 과정
할당 되어있는 블록의 경우에는 연결리스트에서 관리하지 않기 때문에 prev, next 정보는 필요하지 않다.
Explicit Allocator 메모리 할당 해제과정
Explicit Allocator 기본 설계
함수에 필요한 것들이 대부분이 무언가를 넣는 것이기 때문에 매크로를 만들어 놓는 것이 좋다. 프로그램 전반적으로 계속 쓰일 것이기 때문이다. 따라서 맨 처음 변수 혹은 매크로를 설정해야 한다. Explicit은 Implicit와 전반적인 매크로가 비슷하다.
매크로 설정
위의 매크로들은 모두 반복적으로 쓰이는 변수나 함수를 일반화 시켜놓은 것이다. 코드는 다음과 같다.
#define WSIZE 4
#define DSIZE 8
#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)) - DSIZE)
#define NEXT_BLKP(bp) (((char *)(bp) + GET_SIZE((char *)(bp) - WSIZE)))
#define PREV_BLKP(bp) (((char *)(bp) - GET_SIZE((char *)(bp) - DSIZE)))
// 추가된 선언
/* Given ptr in free list, get next and previous ptr in the list */
#define GET_NEXT_PTR(bp) (*(char **)(bp + WSIZE))
#define GET_PREV_PTR(bp) (*(char **)(bp))
/* Puts pointers in the next and previous elements of free list */
#define SET_NEXT_PTR(bp, qp) (GET_NEXT_PTR(bp) = qp)
#define SET_PREV_PTR(bp, qp) (GET_PREV_PTR(bp) = qp)
static void *extend_heap(size_t words);
static void *coalesce(void *bp);
static void *find_fit(size_t asize);
static void place(void *bp, size_t asize);
static void *heap_listp;
// 추가된 함수
static void insert_in_free_list(void *bp);
static void remove_from_free_list(void *bp);
static char *free_list_start;
mm_init
기본적으로 mm_init은 할당기를 초기화하고 성공이면 0, 실패면 -1을 리턴한다. 초기화 하는 과정에서 가용 리스트의 prologue header, prologue footer, epilogue header, prev, next를 만드는 과정이 포함된다.
mm_init
함수에 필요한 것
int mm_init(void) {
if ((heap_listp = mem_sbrk(8*WSIZE)) == (void *)-1) { return -1; }
PUT(heap_listp, 0);
PUT(heap_listp + (1*WSIZE), PACK(2*DSIZE, 1)); /* Prologue header */
PUT(heap_listp + (2 * WSIZE), NULL); /* prev free block pointer 는 null */
PUT(heap_listp + (3 * WSIZE), NULL); /* next free block pointer 는 null */
PUT(heap_listp + (2*WSIZE), PACK(2*DSIZE, 1)); /* Prologue footer */
PUT(heap_listp + (3*WSIZE), PACK(0, 1)); /* Epilogue header */
free_list_start = heap_listp + (2*WSIZE);
if (extend_heap(CHUNKSIZE/WSIZE) == NULL) { return -1; }
return 0;
}
extend_heap
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));
return coalesce(bp);
}
extend_heap의 용도는 크게 다음과 같다.
코드를 보면
size = (words % 2) ? (words + 1) * WSIZE : words * WSIZE;
8의 배수로 size값을 맞춰준다. mem_sbrk(size)
)coalesce
static void *coalesce(void *bp) {
//PREV_BLK(bp) == bp: epilogue block을 만났을 떄. Extend했을 때 epilogue를 만나는 유일한 경우
size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp))) || PREV_BLKP(bp) == bp;
size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
size_t size = GET_SIZE(HDRP(bp));
/* case 1 : 이전, 다음 블록 모두 할당되어있다면 */
if (prev_alloc && next_alloc) {
insert_in_free_list(bp);
return bp;
/* case 2 : 이전 블록은 할당되어있고, 다음 블록은 가용상태라면 */
} else if (prev_alloc && !next_alloc) {
size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
remove_from_free_list(NEXT_BLKP(bp));
PUT(HDRP(bp), PACK(size, 0));
PUT(FTRP(bp), PACK(size, 0));
/* case 3 : 이전 블록은 가용상태이고, 다음 블록이 할당상태라면 */
} else if (!prev_alloc && next_alloc) {
size += GET_SIZE(HDRP(PREV_BLKP(bp)));
remove_from_free_list(PREV_BLKP(bp));
PUT(FTRP(bp), PACK(size, 0));
PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
bp = PREV_BLKP(bp);
/* case 4 : 이전, 다음 블록 모두 가용 상태라면 */
} else {
size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp)));
remove_from_free_list(NEXT_BLKP(bp));
remove_from_free_list(PREV_BLKP(bp));
PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
bp = PREV_BLKP(bp);
}
insert_in_free_list(bp);
return bp;
}
coalesce
함수에서는 현재 블록이 살필 수 있는 4가지 케이스를 다룬다.
mm_malloc
void *mm_malloc(size_t size) {
size_t asize;
size_t extendsize;
char *bp;
/* Ignore spurious requests */
if (size == 0) { return NULL; }
if (size <= DSIZE) {
asize = 2*DSIZE;
} else {
asize = DSIZE * ((size + (DSIZE) + (DSIZE-1)) / DSIZE);
}
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;
}
mm_malloc
에서는 얼마나 블록을 할당할지에 대한 사이즈를 인자로 받는다. 사이즈는 임의로 들어올 수 있기 때문에 정렬을 위해 함수 내부에서 사이즈를 조정해야 한다. 그리고 할당을 할 수 없을 때는 힙을 확장해 할당할 수 있는 자리를 마련해주어야 한다.
할당기가 요청한 크기를 조정하면 할당할 가용 블록이 가용 리스트에 있는지 탐색한다(find_fit
). 맞는 블록을 찾으면 블록을 배치한다. 필요하면 기존 블록을 분할한다(place
). 맞는 블록을 찾지 못하면 힙을 늘리고 다시 배치한다.
find_fit
static void *find_fit(size_t asize) {
/* First-fit search */
void *bp;
for (bp = free_list_start; !GET_ALLOC(HDRP(bp)); bp = GET_NEXT_PTR(bp)) {
if (asize <= (size_t)GET_SIZE(HDRP(bp))) {
// last_malloced_size = asize;
return bp;
}
}
return NULL;
}
find_fit
함수의 코드를 보면
GET_NEXT_PTR
함수를 이용해 다음 블록으로 이동한다.place
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));
remove_from_free_list(bp);
bp = NEXT_BLKP(bp);
PUT(HDRP(bp), PACK(csize-asize, 0));
PUT(FTRP(bp), PACK(csize-asize, 0));
coalesce(bp);
} else {
PUT(HDRP(bp), PACK(csize, 1));
PUT(FTRP(bp), PACK(csize, 1));
remove_from_free_list(bp);
}
}
place
함수는 위치와 할당할 크기를 인자로 받는다.
우선 넣을 위치의 블록 사이즈를 계산하고 할당할 크기보다 블록 사이즈가 크면 나머지 부분은 가용블록으로 쪼갠다.(그대로 놔두면 영역 낭비가 된다.)
블록 사이즈가 크면, 블록을 할당하고(블록 헤더, 푸터 위치를 재조정) 나머지 부분을 쪼갠다. 그렇지 않으면 블록을 할당만 한다.
explicit는 할당을 하면 가용 상태 리스트에서 해당 블록을 제거한다(remove_from_free_list
).
mm_free
void mm_free(void *bp) {
if (bp == NULL) { return; }
size_t size = GET_SIZE(HDRP(bp));
PUT(HDRP(bp), PACK(size, 0));
PUT(FTRP(bp), PACK(size, 0));
coalesce(bp);
}
free
함수는 반환할 블록의 위치를 인자로 받는다. 그리고 해당 블록의 가용 여부를 0으로 변경한다. 여기서 핵심은 블록을 가용 리스트에서 제거하는게 아니라 가용상태로 만들어버린다는 것이다.
블록을 반환한 후 가용 블럭이 생기면 항상 연결하는 함수를 실행해서 연결 가능한 블록을 챙겨야 한다.
realloc
void *mm_realloc(void *ptr, size_t size) {
if ((int)size <= 0) {
mm_free(ptr);
return NULL;
} else {
size_t old_size = GET_SIZE(HDRP(ptr));
size_t new_size = size + (2*WSIZE);
if (new_size <= old_size) {
return ptr;
} else {
size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(ptr)));
size_t csize;
if (!next_alloc && ((csize = old_size + GET_SIZE(HDRP(NEXT_BLKP(ptr))))) >= new_size) {
remove_from_free_list(NEXT_BLKP(ptr));
PUT(HDRP(ptr), PACK(csize, 1));
PUT(FTRP(ptr), PACK(csize, 1));
return ptr;
} else {
void *new_ptr = mm_malloc(new_size);
place(new_ptr, new_size);
memcpy(new_ptr, ptr, new_size);
mm_free(ptr);
return new_ptr;
}
}
}
}
realloc
에서는 크기를 조정할 블록의 위치와 요청 사이즈를 인자로 받는다. 그후 malloc(size)
을 통해 요청 사이즈만큼 블록을 할당한다.
여기서 핵심은 이미 할당된 블록의 사이즈를 직접 건드리는게 아니라, 임시로 요청 사이즈만큼의 블록을 만들고 현재의 블록을 반환하는 것이다. 그러면 사이즈 변경한 효과가 나기 때문이다. 또한 블록 내 데이터 또한 똑같이 옮겨야 하니 C라이브러리에 있는 memcpy
함수를 활용한다. 그리고 기존 블록을 반환한다.
insert_in_free_list
static void insert_in_free_list(void *bp) {
SET_NEXT_PTR(bp, free_list_start);
SET_PREV_PTR(free_list_start, bp);
SET_PREV_PTR(bp, NULL);
free_list_start = bp;
}
insert_in_free_list
에서는 블록의 위치를 인자로 받는다.
여기서 핵심은 연결리스트에 새로 추가되는 블록이기 때문에 이전 블록의 위치를 저장하고 있는 free_list_start
을 통해 서로 가르킬 수 있게 값을 넣어준다. 연결리스트에 젤 앞은 가르킬 블록이 없기 때문에 NULL값을 넣어준다. 마지막으로 free_list_start
을 bp의 값으로 최신화 해준다.
remove_from_free_list
static void remove_from_free_list(void *bp) {
// 다음 블록이 있다면
if (GET_PREV_PTR(bp)) {
// 다음 블록의 주소에, 이전 블록의 주소를 넣는다.
SET_NEXT_PTR(GET_PREV_PTR(bp), GET_NEXT_PTR(bp));
// 다음 블록이 없다면, 즉 자신이 젤 앞 블록이라면
} else {
// 이전 노드를 젤 앞 블록으로 만들어준다.
free_list_start = GET_NEXT_PTR(bp);
}
SET_PREV_PTR(GET_NEXT_PTR(bp), GET_PREV_PTR(bp));
}
remove_from_free_list
에서는 제거할 블록의 위치를 인자로 받는다.
여기서 핵심은 연결리스트에서 제거하는 블록이기 때문에 블록의 앞, 뒤를 다 확인해야 한다. 만약 제거할 블록의 다음 블록이 있다면, 다음 블록의 주소에 이전 블록의 주소를 넣는다. 그런데 제거할 블록의 다음 블록이 없다면, 즉 제거할 블록이 젤 앞 블록이라면, 이전 노드를 젤 앞 블록으로 만들어준다.
Malloc Lap 구현 Github 링크
Malloc Lap