크래프톤 정글(C언어 WEEK 5 ~ 8)

묵시적 리스트 구현(Next Fit)[56점]

devkty 2025. 4. 30. 03:33
728x90

묵시적 리스트 (Next Fit)[56점]

기본적으로 전체틀은 바뀌지 않습니다.
First Fit에서 Next Fit으로 바꾸려면, 기존의 코드에서 find_fit 함수만 바꾸면 됩니다. 그러나 저는 얼마 안가 해당 함수만 바꾸면 되는 걸 알게 되었습니다.

합병을 담당하는 coalesce에 맨 밑 리턴 값에 last_bp = bp; 을 추가하여야 합니다. 또한 last_bp 변수를 어떤 함수에서든 쓸수 있게 전역변수로 선언해줘야 합니다.(초깃값 NULL)

전역 변수 추가 및 coalesce 수정

.
.
.
// 다음 블록의 포인터
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(((char *)(bp) - WSIZE)))
// 이전 블록의 포인터
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE(((char *)(bp) - DSIZE)))

/* 추가할 부분 */ static void *last_bp = NULL;   // next fit을 위한 선언

// 상수 시간에 인접 가용 블록 병합하기
static void *coalesce(void *bp) {
    .
    .
    .
    else {                                                       // Case 3: 이전 블록은 가용(free), 다음 블록은 할당된 경우
        size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp)));   // 이전 블록 + 현재 블록 + 다음 블록을 모두 합쳐 크기를 늘림
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));                 // 이전 블록의 헤더를 새 size로 업데이트 (free 상태)
        PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));                 // 다음 블록의 푸터를 새 size로 업데이트 (free 상태)
        bp = PREV_BLKP(bp);                                      // bp를 병합된 블록(이전 블록) 쪽으로 이동
    }
    /* 추가할 부분 */ last_bp = bp;     // next fit을 위한 bp 포인터 값 반환
    return bp;       // 병합이 끝난 블록의 포인터를 반환
}

find_fit 수정

static void *find_fit(size_t asize)
{
    if (last_bp == NULL)                        // NULL이면
        last_bp = mem_heap_lo() + 2 * WSIZE;    // 원래대로 힙 시작 주소에서 2워드(8바이트)를 건너뛴 곳(첫 블록)부터 탐색 시작

    void *bp = last_bp;                // 가장 최근의 값을 
    void *heap_end = mem_heap_hi();    // memlib에 있는 최근 주소 바이트를 가져옴

    // 1단계: last_bp부터 힙 끝까지
    while ((char *)bp <= (char *)heap_end) {                          
    // 현재 포인터(bp)가 힙 끝 주소(heap_end)를 넘지 않았는지 확인
        if (!GET_ALLOC(HDRP(bp)) && asize <= GET_SIZE(HDRP(bp))) {    
        // 현재 블록이 가용 상태(free)이며 현재 블록의 크기가 요청 크기(asize) 이상이면 
            last_bp = bp;   // 마지막 탐색 성공 위치를 저장하여 다음 탐색의 시작 지점으로 삼음
            return bp;      // 조건을 만족하는 블록을 찾았으므로 해당 포인터 반환
        }
        bp = NEXT_BLKP(bp); // 조건을 만족하지 않으면 다음 블록으로 이동
    }

    // 2단계: 힙 시작부터 last_bp까지
    // 힙의 실제 첫 블록은 padding + prologue(2워드)를 지난 곳이므로 mem_heap_lo() + 8에서 시작
    for (bp = mem_heap_lo() + 2 * WSIZE; (char *)bp < (char *)last_bp; bp = NEXT_BLKP(bp)) {
        if (!GET_ALLOC(HDRP(bp)) && asize <= GET_SIZE(HDRP(bp))) {     // 위와 동일한 조건으로 블록 검사
            last_bp = bp;  // 다음 탐색을 위한 위치 갱신
            return bp;     // 조건을 만족하는 블록 반환
        }
    }
    return NULL;           // 못 찾았을 경우
}

위의 총 3가지 부분을 수정하면 정상적으로 Next Fit을 구현할 수 있습니다.

find_fit 함수는 기존의 First Fit 코드와 다르게, 크게 두가지 케이스로 나눌 수 있습니다.

  1. 최근 수정된 포인터 last_bp부터 힙 끝까지 확인해서 적절한 블록을 추가하는 케이스
  2. 힙 시작부터 last_bp까지 확인해서 적절한 블록을 추가하는 케이스

→ 위의 두 사항을 참고하여 수정하면 좋을 것 같습니다. 추가적으로 last_bp가 없다면 기존 코드대로 진행하는 if문도 추가하면 정상적으로 구현 가능합니다.

리눅스 구축환경에 따라 다르겠지만, 저의 시놀로지 도커 Ubuntu 18.04에서는 56점이 나왔습니다. 사양과 버전에 따라 더 높게 나오는 것 같습니다.

Next Fit이 왜 우수한 점수를 보여주는가?

bp = mem_heap_lo() + 2 * WSIZE;   // 항상 힙 처음부터 탐색 시작
while (GET_SIZE(HDRP(bp)) > 0) {
if (!GET_ALLOC(HDRP(bp)) && asize <= GET_SIZE(HDRP(bp)))
return bp;
bp = NEXT_BLKP(bp);
}

위의 코드는 First Fit의 일부 코드를 가져온 것입니다. 보면 알 수 있듯이 항상 힙 처음부터 탐색을 시작하는 것을 알 수 있습니다. 그러면 두가지 문제가 발생합니다.

  • 자주 호출되면 앞부분 블록에 집중적으로 할당됨힙 앞쪽 단편화 심화
  • 탐색 시간이 길어질 수 있음
// 탐색은 마지막에 할당된 위치 이후부터 시작
while ((char *)bp <= (char *)heap_end) {
...
}
for (bp = mem_heap_lo() + 2 * WSIZE; (char *)bp < (char *)last_bp; bp = NEXT_BLKP(bp)) {
...
}

그러나 Next Fit은 다음과 같은 부분으로 최근 할당된 위치 기준으로 탐색하기 때문에, First Fit 보다 더 나은 성능을 보여줍니다.

Next Fit은 다음과 같은 이점이 있습니다.

  • 최근에 할당된 위치 기준으로 가용 블록 분포를 넓게 활용함
  • 힙 전체를 균등하게 사용하려는 특성이 있음
  • 탐색 시간 단축 가능 → 특히 할당 요청이 빈번할 때 성능이 좋아짐

트러블 슈팅 시 다음 벨로그를 참고했습니다.

https://velog.io/@dlgudwns1207/Malloc-Lab-first-fit-%EC%9D%84-next-fit-%EB%A1%9C-%EB%B0%94%EA%BE%B8%EB%A9%B0-%EB%A7%8C%EB%82%9C-%EB%AC%B8%EC%A0%9C

참고용 Next Fit 전체 코드

    /*
     * mm-naive.c - 가장 빠르지만 메모리 효율이 가장 낮은 malloc 패키지입니다.
     * 이 단순 접근 방식에서는 brk 포인터를 단순히 증가시켜 블록을 할당합니다.
     * 블록은 순수한 페이로드입니다. 헤더나 푸터가 없습니다. 블록은 병합되거나 재사용되지 않습니다.
     * realloc은 mm_malloc과 mm_free를 사용하여 직접 구현됩니다.
     *
     * 참고: 이 헤더 주석을 해결책에 대한 간략한 설명을 제공하는 자체 헤더 주석으로 바꾸세요.
     */
    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    #include <unistd.h>
    #include <string.h>

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

    /*********************************************************
     * NOTE TO STUDENTS: Before you do anything else, please
     * provide your team information in the following struct.
     ********************************************************/
    team_t team = {
        /* Team name */
        "5team",
        /* First member's full name */
        "Kim Taeyong",
        /* First member's email address */
        "bovik@cs.cmu.edu",
        /* Second member's full name (leave blank if none) */
        "",
        /* Second member's email address (leave blank if none) */
        ""
    };

    // 기본 상수
    #define WSIZE 4                // 글자, 헤더, 풋터 크기 4byte
    #define DSIZE 8                // 더블(부동소수점) 글자 크기 8byte
    #define CHUNKSIZE (1<<12)      // 힙 확장을 위한 기본 크기(초기의 빈 블록의 크기)

    // 매크로
    #define MAX(x, y)  ((x) > (y) ? (x) : (y))

    // size와 할당 비트를 결합, header와 footer에 저장할 값
    #define PACK(size, alloc)  ((size) | (alloc))

    // p가 참조하는 워드 반환(포인터라서 직접 역참조 불가능 -> 타입 캐스팅)
    #define GET(p)        (*(unsigned int *)(p))
    // p에 val 저장
    #define PUT(p, val)   (*(unsigned int *)(p) = (val))

    // 사이즈 (~0x7: ...11111000, '&' 연산으로 뒤에 세자리 없어짐)
    #define GET_SIZE(p)   (GET(p) & ~0x7)
    // 할당 상태
    #define GET_ALLOC(p)  (GET(p) & 0x1)

    // Header 포인터
    #define HDRP(bp)      ((char *)(bp) - WSIZE)
    // Footer 포인터 (Header의 정보를 참조해서 가져오기 때문에, Header의 정보를 변경했다면 변경된 위치의 Footer가 반환됨에 유의)
    #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)))

    static void *last_bp = NULL;   // next fit을 위한 선언

    // 상수 시간에 인접 가용 블록 병합하기
    static void *coalesce(void *bp) {
        size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp)));  // 이전 블록 할당 상태 가져옴(0 = free, 1 = allocated)
        size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));  // 다음 블록 할당 상태 가져옴(0 = free, 1 = allocated)
        size_t size = GET_SIZE(HDRP(bp));                    // 현재 블록 사이즈

         /// case 1. 이전 블록과 다음 블록 모두 할당된 경우 (병합 불가)
        if (prev_alloc && next_alloc) {
            return bp;
        }

        // Case 2: 이전 블록은 할당, 다음 블록은 가용(free)한 경우
        else if (prev_alloc && !next_alloc) {        
            size += GET_SIZE(HDRP(NEXT_BLKP(bp)));    // 현재 블록과 다음 블록을 합쳐 크기를 늘림
            PUT(HDRP(bp), PACK(size, 0));             // 현재 블록의 헤더를 새 size로 업데이트 (free 상태)
            PUT(FTRP(bp), PACK(size, 0));             // 합쳐진 블록의 푸터를 새 size로 업데이트
        }

        /// case 3: 이전 블록은 가용(free), 다음 블록은 할당된 경우
        else if (!prev_alloc && next_alloc) {      
            size += GET_SIZE(HDRP(PREV_BLKP(bp)));    // 이전 블록과 현재 블록을 합쳐 크기를 늘린다
            PUT(FTRP(bp), PACK(size, 0));             // 이전 블록의 헤더를 새 size로 업데이트(free)
            PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));  // 현재 블록의 푸터를 새 size로 업데이트
            bp = PREV_BLKP(bp);                       // bp를 병합된 블록(이전 블록) 쪽으로 이동
        }

        else {                                                       // Case 3: 이전 블록은 가용(free), 다음 블록은 할당된 경우
            size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp)));   // 이전 블록 + 현재 블록 + 다음 블록을 모두 합쳐 크기를 늘림
            PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));                 // 이전 블록의 헤더를 새 size로 업데이트 (free 상태)
            PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));                 // 다음 블록의 푸터를 새 size로 업데이트 (free 상태)
            bp = PREV_BLKP(bp);                                      // bp를 병합된 블록(이전 블록) 쪽으로 이동
        }
        last_bp = bp;     // next fit을 위한 bp 포인터 값 반환
        return bp;       // 병합이 끝난 블록의 포인터를 반환
    }

    // 힙 확장: 새 가용 블록으로 힙을 확장한다.
    static void *extend_heap(size_t words)
    {
        char *bp;
        size_t size;

        // 더블 워드 정렬 유지
        // size: 확장할 크기
        size = (words % 2) ? (words + 1) * WSIZE : words * WSIZE;   // 더블워드의 가장 가까운 배수로 반올림(홀수면 1 더해서 곱한다)
        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);     // 병합 후 coalesce 함수에서 리턴된 블록 포인터 반환
    }

    /////////////////
    // 검색 후 가용블록 반환하기 (First fit)
    // 가용 리스트를 순차적으로 검색해서, 요청 크기 이상의 가용 블록을 찾아 반환한다
    static void *find_fit(size_t asize)
    {
        if (last_bp == NULL)                        // NULL이면
            last_bp = mem_heap_lo() + 2 * WSIZE;    // 원래대로 힙 시작 주소에서 2워드(8바이트)를 건너뛴 곳(첫 블록)부터 탐색 시작

        void *bp = last_bp;                // 가장 최근의 값을 
        void *heap_end = mem_heap_hi();    // memlib에 있는 최근 주소 바이트를 가져옴

        // 1단계: last_bp부터 힙 끝까지
        while ((char *)bp <= (char *)heap_end) {                          // 현재 포인터(bp)가 힙 끝 주소(heap_end)를 넘지 않았는지 확인
            if (!GET_ALLOC(HDRP(bp)) && asize <= GET_SIZE(HDRP(bp))) {    // 현재 블록이 가용 상태(free)이며 현재 블록의 크기가 요청 크기(asize) 이상이면 
                last_bp = bp;   // 마지막 탐색 성공 위치를 저장하여 다음 탐색의 시작 지점으로 삼음
                return bp;      // 조건을 만족하는 블록을 찾았으므로 해당 포인터 반환
            }
            bp = NEXT_BLKP(bp); // 조건을 만족하지 않으면 다음 블록으로 이동
        }

        // 2단계: 힙 시작부터 last_bp까지
        // 힙의 실제 첫 블록은 padding + prologue(2워드)를 지난 곳이므로 mem_heap_lo() + 8에서 시작
        for (bp = mem_heap_lo() + 2 * WSIZE; (char *)bp < (char *)last_bp; bp = NEXT_BLKP(bp)) {
            if (!GET_ALLOC(HDRP(bp)) && asize <= GET_SIZE(HDRP(bp))) {     // 위와 동일한 조건으로 블록 검사
                last_bp = bp;  // 다음 탐색을 위한 위치 갱신
                return bp;     // 조건을 만족하는 블록 반환
            }
        }
        return NULL;           // 못 찾았을 경우
    }

    // 효과적으로 분할하기
    // 블록을 할당할 때, 남는 공간이 충분하면 블록을 분할(split) 하고, 그렇지 않으면 그냥 전체 블록을 할당한다
    static void place(void *bp, size_t asize)
    {
        size_t csize = GET_SIZE(HDRP(bp));     // 현재 블록의 전체 크기 가져오기

        if ((csize - asize) >= (2 * DSIZE))    // Case 1: 블록을 분할할 수 있는 경우 (남은 크기가 최소 블록 크기 이상일 때)
        {
            // 현재 블록에 asize 크기만큼 할당 표시
            PUT(HDRP(bp), PACK(asize, 1));
            PUT(FTRP(bp), PACK(asize, 1));

            // 남은 부분을 새로운 가용 블록으로 설정
            PUT(HDRP(NEXT_BLKP(bp)), PACK((csize - asize), 0)); 
            PUT(FTRP(NEXT_BLKP(bp)), PACK((csize - asize), 0));
        }

        else                                   // Case 2: 블록을 분할하지 않고 통째로 사용하는 경우
        {
            // 현재 블록 전체를 할당 표시
            PUT(HDRP(bp), PACK(csize, 1));
            PUT(FTRP(bp), PACK(csize, 1));
        }
    }
    //////////////

    // mm_init - initialize the malloc package.
    // 힙 생성하기: 패딩+프롤로그 헤더+프롤로그 풋터+에필로그 풋터를 담기위해 4워드 크기합을 구성함
    // 생성 후 CHUNKSIZE만큼 추가 메모리를 요청해 힙의 크기를 확장
    int mm_init(void)
    {
        char *heap_listp;    // 초기 힙을 생성한다 

        if ((heap_listp = mem_sbrk(4*WSIZE)) == (void *)-1)    // 4워드 크기의 힙 생성, heap_listp에 힙의 시작 주소값 할당
            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);

        if (extend_heap(CHUNKSIZE/WSIZE) == NULL)      // free블록으로 빈 힙을 CHUNKSIZE bytes로 확장
            return -1;
        return 0;
    }

    // mm_malloc - Allocate a block by incrementing the brk pointer.
    // Always allocate a block whose size is a multiple of the alignment.
    // 가용 블록 할당하기
    // brk 포인터를 증가시켜 블록을 할당합니다. 항상 정렬 크기의 배수인 블록을 할당합니다.
    void *mm_malloc(size_t size)
    {
        size_t asize;           // 요청된 크기를 조정한 블록 크기 (payload + overhead 포함)
        size_t extendsize;      // heap을 확장할 사이즈
        char *bp;               // 새로 할당할 블록 포인터

        // 잘못된 요청 처리: size가 0이면 NULL 반환
        if(size == 0)
            return NULL;

        // 요청 크기 조정 (payload + header/footer 포함, alignment 맞추기)
        if (size <= DSIZE)        // 8바이트 이하 시
            asize = 2*DSIZE;      // 최소 블록 크기 16바이트 할당 (헤더 4 + 푸터 4 + 저장공간 8)
        else
            asize = DSIZE * ((size + (DSIZE) + (DSIZE-1)) / DSIZE);     // size에 overhead 추가하고, DSIZE 단위로 8의 배수로 올림 (align)

        // Step 1: 가용 리스트에서 알맞은 블록 찾기
        if ((bp = find_fit(asize)) != NULL) {
            place(bp, asize);     // 블록을 할당하고
            return bp;            // 포인터 반환
        }

        // Step 2: 가용 블록이 없으면 힙을 확장
        extendsize = MAX(asize, CHUNKSIZE);     // 최소 CHUNKSIZE만큼은 확장
        if ((bp = extend_heap(extendsize/WSIZE)) == NULL)
            return NULL;    // 확장 실패시 NULL 반환

        place(bp, asize);   // 새로 확장한 블록에 메모리 배치
        return bp;          // 새 블록 포인터 반환
    }

    // mm_free - Freeing a block does nothing.
    // 블록 반환하기
    void mm_free(void *bp)
    {
        size_t size = GET_SIZE(HDRP(bp));  // 현재 블록(bp)의 헤더를 읽어서 블록 크기(size)를 가져옴

        PUT(HDRP(bp), PACK(size, 0));      // 블록의 헤더(Header) 정보를 업데이트: size 그대로, 할당 비트(allocation bit)는 0 (free 상태)
        PUT(FTRP(bp), PACK(size, 0));      // 블록의 푸터(Footer) 정보도 업데이트: size 그대로, 할당 비트는 0 (free 상태)
        coalesce(bp);                      // 인접한 가용 블록들과 병합(Coalesce) 시도
    }

    // mm_realloc - Implemented simply in terms of mm_malloc and mm_free
    // 재할당 블록 재할당하기
    // 기존 블록 내용을 새 블록에 복사한 후 기존 블록을 해제, 새 포인터 반환
    void *mm_realloc(void *ptr, size_t size)
    {
        void *oldptr = ptr;    // 기존 블록 포인터
        void *newptr;          // 새로 할당받을 블록 포인터
        size_t copySize;       // 복사할 데이터 크기

        // Step 1: 새 크기만큼 메모리를 할당
        newptr = mm_malloc(size);
        if (newptr == NULL)    // 메모리 할당 실패 시 NULL 반환
            return NULL;

        // Step 2: 기존 블록의 크기 읽기
        copySize = GET_SIZE(HDRP(oldptr));

        // Step 3: 복사할 크기를 요청 크기로 조정 (원래 블록이 더 크면 size까지만 복사)
        if (size < copySize)
            copySize = size;

        // Step 4: 데이터 복사
        memcpy(newptr, oldptr, copySize);

        // Step 5: 기존 블록 반환 (free)
        mm_free(oldptr);

        return newptr;         // 새 블록 포인터 반환
    }
728x90