동적 메모리 할당 | Malloc Lab | (3) 기초적인 할당기 작동 원리, 구현
컴퓨터 시스템 책의 9.9장에 설명되어 있는 기초적인 버전의 할당기는 다음과 같은 방식을 채택하고 있다.
해당 버전의 작동 원리, 코드를 이해해보자.
우리가 흔히 보는 가상 메모리 그림에서 힙 공간에 돋보기를 대본다고 생각해보자.
그럼 힙 공간은 메모리 블록들의 배열로 표현할 수 있다. (이 '블록들'은 앞 포스팅에서 얘기한 묵시적 가용 리스트 등의 자료구조이다.)
완성된 mm.c 파일의 코드를 기반으로 보다 자세하게 이해해보자
매크로는 묵시적 가용 리스트를 조작하기 위해 미리 정의해놓은 명령어라고 보면 된다.
(매크로는 함수와 달리 전처리 때 처리되며, 코드 삽입의 형태로 파일 내부에 들어가게 된다.)
아래는 함수들을 구현하는데 쓰이는 매크로들인데, 매크로부터 보면 이해가 안된다.
때문에 함수 코드를 먼저 보면서, 특정 매크로가 나온다면 다시 올라와서 어떤 메커니즘을 지원하는지 확인해보는 걸 추천!
/* 싱글워드 (4) or 더블워드 (8) 정렬 */
#define ALIGNMENT 8
/* 정렬 기준의 배수가 되도록 반올림 */
#define ALIGN(size) (((size) + (ALIGNMENT-1)) & ~0x7)
#define SIZE_T_SIZE (ALIGN(sizeof(size_t)))
/* 가용 리스트 조작을 위한 기본 상수 및 매크로 정의 */
#define WSIZE 4 /* Word and head/footer size (bytes) */
#define DSIZE 8 /* Double word size (bytes) */
#define CHUNKSIZE (1<<12) /* Extend heap by this amount (bytes)*/
/* 1을 12bit 왼쪽으로 shift. 즉, CHUNKSIZE = 2^12 = 4096 bytes
많은 시스템에서 페이지 크기는 4kb. 힙 확장은 일반적으로 페이지 크기의 배수로 확장함. */
#define MAX(x, y) ((x) > (y) ? (x) : (y))
/* 헤더에 넣을 정보 생성 (만약 size=16byte, 할당되었다면 PACK(16,1) = 17) */
#define PACK(size, alloc) ((size) | (alloc))
/* 주소 p에 접근하여 워드를 read, write */
#define GET(p) (*(unsigned int *)(p))
#define PUT(p, val) (*(unsigned int *)(p) = (val))
/* 주소 p에 접근하여 size, allocated fields 읽기 */
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)
/* bp(block ptr)가 주어지면 header, footer의 주소를 계산 */
#define HDRP(bp) ((char *)(bp) - WSIZE)
#define FTRP(bp) ((char *)(bp) + GET_SIZE(HDRP(bp)) - DSIZE)
/* bp(block ptr)가 주어지면, 이전 블록과 이후 블록의 주소를 계산 */
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE((char *)(bp) - WSIZE))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE((char *)(bp) - DSIZE))
static void *heap_listp;
실제 할당기의 동작을 담당하는 함수부를 살펴보자.
가장 기본이 되는 init(), malloc(), free() 함수가 있고
이들의 수행을 위해 필요한 서브 함수 extend_heap(), coalesce(), find_fit(), place()가 있다.
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)); /* 프롤로그 header */
PUT(heap_listp + (2*WSIZE), PACK(DSIZE, 1)); /* 프롤로그 footer */
PUT(heap_listp + (3*WSIZE), PACK(0, 1)); /* 에필로그 header */
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;
}
mm_init은 처음에 힙 공간을 할당받는 함수이다. 할당받은 공간에 최초의 가용 블록 포맷을 세팅한다.
/* extend_heap() extends the heap with a new free block */
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;
}
/* 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);
}
extend_heap은 적합한 블록 공간이 없을 때, 1) 새로운 힙 공간을 할당받고 2) 그곳에 블록 포맷을 세팅하는 함수이다.
할당 과정이 끝난 이후에, 마지막으로 이전 블록이 가용(free) 상태이면 이전 블록과 연결 작업을 수행한다.
/* mm_malloc - brk 포인터를 증가시켜 블록을 할당.
항상 크기가 alignment의 배수인 블록을 할당한다. */
void *mm_malloc(size_t size)
{
size_t asize; /* Adjusted block size */
size_t extendsize; /* Amount to extend heap if no fit */
char *bp;
/* fake 요청은 무시 */
if(size == 0){
return NULL;
}
/* 오버헤드 및 정렬 요구 사항을 포함하도록 블록 크기를 조정 */
if (size <= DSIZE){
asize = 2*DSIZE;
}
else {
asize = DSIZE * ((size + (DSIZE) + (DSIZE-1)) / DSIZE);
}
/* 적합한 free list 탐색 */
if ((bp = find_fit(asize)) != NULL ){
place(bp, asize); // 찾은 위치에 asize 만큼 메모리 배치
return bp;
}
/* No fit found. 추가 메모리 할당 및 블록 배치 */
extendsize = MAX(asize, CHUNKSIZE);
if ((bp = extend_heap(extendsize/WSIZE)) == NULL){
return NULL;
}
place(bp, asize);
return bp;
}
malloc()은 실제 우리가 힙 공간의 메모리 할당 요청을 할 때 동작하는 함수이다.
우선 메모리 할당은 항상 정렬 기준을 맞춘다. 그래야 전체적인 포맷이 유지되어 검색과 할당이 용이하기 때문.
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)){
// 할당되지 않았고, asize보다 크다면
if (!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp)))){
return bp;
}
}
return NULL; /* No fit */
}
first fit 전략에서의 find_fit은 힙의 처음부터 끝까지 쭉 순회한다.
이 경우, 이용도 측면에서는 이득을 보지만 쓰루풋 측면에서는 손해를 볼 수 있다.
왜냐하면 작은 블록이든 큰 블록이든 앞에서부터 할당하기 때문에, 할당과 반환을 반복하다보면 앞에 애매한 빈 공간이 많이 생기게 되기 때문이다. 따라서 나중에는 큰 블록 할당 요청이 들어올 경우 매번 뒤쪽까지 탐색해야 하므로 시간복잡도 측면에서 손실이 발생한다.
// 분할 전략: 필요한 만큼만 할당하고 남은 부분은 가용 블록으로 분할
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)); // 필요한 부분을 제외한 나머지는 free로 분리
PUT(FTRP(bp), PACK(csize-asize, 0));
}
else {
PUT(HDRP(bp), PACK(csize, 1));
PUT(FTRP(bp), PACK(csize, 1));
}
}
place는 해당 free list에 요청받은 size를 할당하고, 분할 전략에 따라 남은 공간은 새로운 가용 블록으로 분리하는 함수이다.
/* free 연산 + 즉시 연결 전략 */
void mm_free(void *bp)
{
size_t size = GET_SIZE(HDRP(bp));
PUT(HDRP(bp), PACK(size, 0));
PUT(FTRP(bp), PACK(size, 0));
coalesce(bp); // 즉시 연결
}
free는 해당 가용 공간의 할당여부를 1 -> 0로 바꿔준다.
그 다음 즉시 연결 전략에 따라 바로 coalesce()를 수행하고 있다.
static void *coalesce(void *bp)
{
// GET_ALLOC: 해당 주소의 마지막 비트 반환 -> 헤더 or 푸터라면, 할당됐으면 1, 아니면 0 반환됨
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문에서 0은 false, 그 외 숫자는 true로 인식
if (prev_alloc && next_alloc){ /* Case 1. 이전 & 다음 not free */
return bp;
}
else if (prev_alloc && !next_alloc){ /* Case 2. 다음 블록 free */
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){ /* Case 3. 이전 블록 free */
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 { /* Case 4. 이전 & 다음 free */
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;
}
앞 뒤 인접 블록을 조사한 다음, 가용 블록이라면 현재 블록과 합쳐 더 큰 하나의 블록으로 만드는 과정이다.
/*
* mm_realloc - mm_malloc 및 mm_free을 통해 간단하게 구현됨
*/
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));
// copySize = *(size_t *)((char *)oldptr - SIZE_T_SIZE);
if (size < copySize)
copySize = size;
memcpy(newptr, oldptr, copySize);
mm_free(oldptr);
return newptr;
}
realloc은 우선 새로운 공간을 malloc으로 할당받고, 이전 메모리 공간에 있던 데이터를 그대로 옮겨오는 전략을 취하고 있다.
위와 같은 설계로 할당기를 구현했을 때 총점 53점이 나왔다. (ubuntu 환경 기준)
utilization(이용도)은 높지만 throughput(처리량)은 많이 낮은 것을 확인할 수 있다.
가상메모리 | (1) 개발자 코드가 물리 메모리에 닿는 과정 (2) | 2023.11.14 |
---|---|
동적 메모리 할당 | Malloc Lab | (4) 할당기 배치 전략 개선 (next fit) (0) | 2023.11.14 |
동적 메모리 할당 | Malloc Lab | (2) 과제 소개 (0) | 2023.11.11 |
동적 메모리 할당 | Malloc Lab | (1) Malloc은 어떻게 구현되는가? (2) | 2023.11.10 |
동적 메모리 할당 | 기본 개념, 메모리 누수, 단편화 (1) | 2023.11.04 |