[Malloc_Lab] 매크로 함수 정리

Laska·2025년 4월 27일
post-thumbnail

malloc_lab 구현 주차에 들어갔다.. 겁먹지 말고 매크로 부터 정리해보려고 한다.


매크로를 쓰는 이유는 뭔데 ?

매크로 함수를 이욯하면, 성능최적화코드 간결화를 얻을 수 있다.

1. 함수 호출 오버헤드를 없애기 위해

  • 일반 함수는 호출 할 때 스택에 매개변수를 복사하고, 컨트롤 플로우를 이동하는 오버헤드가 있다.
  • 매크로는 단순히 코드를 그대로 복붙하는 거라서, 위와 같은 오버헤드가 없다.
    -> 그래서 매우 짧은 코드에서는 매크로가 성능상 유리하다 !

2. 간단한 코드를 편하게 쓰기 위해

  • 예를 들어, MAX(a,b) 처럼 두 값 중 큰값을 구하는 것은 코드가 매우 짧지만 자주 쓰인다. 이러한 함수들을 매번 직접 쓰기 귀찮기도하고, 오버헤드를 줄이기 위해 매크로로 구현해 놓고 계속해서 재사용할 수 있다 !

3. 타입에 상관없이 쓸 수 있다.

  • 매크로는 타입 체크를 하지 않기 때문에 int , float, double 등 어떤 타입이든 입자로 넣을 수 있다. (C의 함수는 타입을 정확히 맞춰야하기 때문에 간편함 !)

하지만 이런 장점들도 있지만, 단점도 있다.

  • 매크로는 단순한 문자열 치환이라서 버그가 숨어들기 쉽다.
  • 괄호를 잘 쓰지 않으면 우선 순위 떄문에 이상한 동작이 나올 수 있다..

대표적인 예시

#define SQUARE(x) x*x   // 잘못된 매크로
int y = SQUARE(1+2);    // 1+2*1+2 = 5, 예상은 9인데...

위와 같은 경우를 막으려면

#define SQUARE(x) ((x)*(x)) // 이렇게 괄호로 묶어줘야 함

요약하면
→ 함수 호출 부담을 없애고, 짧은 코드를 편하게 쓰기 위해 매크로를 쓴다.
(단, 신중하게 써야 하고 요즘은 inline 함수가 매크로를 대체하는 경우도 많다.)


MALLOC 구현을 위한 매크로 정리


1. Alligment 상수

#define ALIGNMENT 8

32비트의 운영체제를 기반으로 malloc을 구현하기 떄문에 기본 ALIGNMENT를 8바이트로 명시해줬다.


2. ALIGN, SIZE_T_SIZE 매크로 (블록 정렬)

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

블록의 SIZE가 들어오면 해당 SIZE를 8의 배수를 올림 연산을 해주는 매크로이다.

  • ~0x7 = 0xFFFFFFF8 (하위 3비트 0 → AND연산 하면 하위 비트 날리고 8의 배수 됨)
    비트 마스킹을 활용한 빠른 나머지 제거 방식

  • size_t 크기를 8의 배수로 올린 값을 반영함(정렬 안전성 확보) 정렬 깨지면 성능 문제 + 크래시 위험


3. WSIZE, DSIZE, CHUNKSIZE 상수 선언

#define WSIZE 4 /* word size (byte)*/
#define DSIZE 8 /* double word SIZE (byte) */
#define CHUNKSIZE (1<<12) /* 초기 가용 블록과 힙 확장을 위한 크기 (byte)*/

WSIZEword 한개의 사이즈인 4바이트를 선언하였고,
DSIZE는 double word 사이즈로 word 2개의 사이즈인 8바이트 선언

CHUNKSIZE는 쉬프트 연산을 통해 2^12 값인 4096을 넣어주었다.

  • 초기 힙의 가용블록은 4096 BYTE 즉 4KB 정도라고 한다. 해당 구현을 위해 2122^{12} 으로 구현하였다.

4. MAX 매크로

#define MAX(x, y) ((x) > (y) ? (x) : (y)) // 둘중 더 큰 값을 반환하는 max 매크로

삼항 연산자를 통하여 MAX함수를 매크로로 구현하였다. 매크로를 구현할 때 주의점을 항상 인지하며, 괄호를 생활화 하자..!


5. PACK 매크로

#define PACK(size, alloc) ((size) | (alloc)) /* 헤더, 푸터를 만들기 위한 매크로*/

size(블록 크기)와 할당 여부(alloc)를 하나의 4바이트 값으로 합치는 매크로이다.

  • size는 보통 8의 배수야 (8바이트 정렬 때문) → 하위 3비트(000)가 무조건 비어있다. alloc은 0 또는 1 (0: free, 1: allocated).

  • 이 둘을 OR 연산으로 합치면 상위 비트는 크기 정보 맨 하위 비트는 할당 여부 정보가 들어감.

  • 해당 매크로로 headerfooter 구현 가능

예시 -> PACK(24, 1) → 0x18 | 0x1 → 0x19


6. GET, PUT 매크로

#define GET(p)      (*(unsigned int *)(p)) // 해당 주소에 저장된 4바이트의 값을 읽어오는 것
#define PUT(p, val) (*(unsigned *)(p) = (val)) //해당 주소에 4바이트의 값을 써주는 것

GET(p) : 주소 P가 가리키는 메모리에서 4바이트(32비트)를 읽어옮

  • 이때 p는 헤더나 푸터를 가리켜야함.
  • 즉, 블록크기 + 할당여부가 저장된 값을 읽어온다.

PUT(p, val) : 주소 P4바이트(32비트) 값을 val로 써줌

  • 헤더나 푸터에 크기와 할당 정보를 기록할 때 사용한다.

7. GET_SIZE, GET_ALLOC 매크로

#define GET_SIZE(p)     (GET(p) & ~0x7) // 하위 3비트 무시 (사이즈만)
#define GET_ALLOC(p)    (GET(p) & 0X1) // 하위 1비트에 1을 넣고 and연산 (할당 여부만)

GET_SIZE(p) : p가 가리키는 헤더/푸터에서 블록 크기만 뽑아냄.

  • 0X7 == 0b111 -> 하위 3비트가 모두 1
  • ~0x7 == 0xffffff...ff8 -> 하위 3비트만 0, 나머지는 1
  • AND연산을 통해 할당플래그(맨 하위 비트)랑 padding bit들을 제거하고, 진짜 size만 가져옴.

GET_ALLOC(p) : p가 가리키는 헤더/푸터에서 할당 여부만 뽑아냄.

  • 하위 1비트(0X1)만 AND 연산해서 0 (free) / 1 (allocated) 을 판별함.

8. HDRP, FTRP 매크로

#define HDRP(bp)    ((char *)(bp) - WSIZE) // payload 기준 4바이트(1워드) 앞으로 가서 헤더 찾기
#define FTRP(bp)    ((char *)(bp) + GET_SIZE(HDRP(bp)) - DSIZE)// payload 기준 블록크기만큼 뒤로 가서 푸터 찾기

HFTP(bp) : bp (payload 시작주소) 에서 4바이트(1워드) 앞으로 이동해서 헤더의 주소를 찾는다.

  • 블록 구조가 [헤더][payload][푸터] 형태이다.
  • payload 바로 앞에 헤더가 4바이트로 있음.
  • WSIZE4바이트 (word size)로 정의되어 있다고 가정함.

FTRP(bp) : bp(payload 시작 주소) 기준으로 푸터의 주소를 찾아줌.

  • GET_SIZE(HDRP(bp)) -> 이 블록의 전체 크기.

  • 거기서 DSIZE(8바이트)를 빼줌.

    왜 8바이트를 빼냐?

    - payload 앞쪽에 4바이트 헤더

    - payload 끝나고 나서 바로 4바이트 푸터

    - 총 8바이트니까!


9. NEXT_BLKP, PREV_BLKP 매크로

#define NEXT_BLKP(bp)   ((char *)(bp) + GET_SIZE(((char *)(bp) - WSIZE)))
#define PREV_BLKP(bp)   ((char *)(bp) - GET_SIZE(((char *)(bp) - DSIZE)))

NEXT_BLKP(bp) : 현재 블록 bp 기준으로 다음 블록의 payload 주소를 구함.

  • 현재 payload에서 현재 블록의 헤더를 찾고 (bp - 4)

  • GET_SIZE(헤더)로 이 블록의 크기를 얻어

  • 그만큼 오른쪽으로 이동하면 다음 블록의 payload가 나옴.


PREV_BLKP(bp) : 현재 블록 bp 기준으로 이전 블록의 payload 주소를 구함.

  • bp - 8 하면 이전 블록의 푸터 주소.

  • GET_SIZE(푸터)를 읽어오면 이전 블록의 크기.

  • 그 크기만큼 왼쪽으로 이동하면 이전 블록의 payload 주소를 알 수 있다.


전체 코드

/* 32비트 운영체제를 사용하기 때문에 8바이트 사용...*/
#define ALIGNMENT 8

/* rounds up to the nearest multiple of ALIGNMENT */
/* size를 8의 배수로 올림 연산을 해주는 것이다.*/
/* (size + 7) : 8로 나누어 떨어지게 만들기 위해 +7
~0x7 = 0xFFFFFFF8 (하위 3비트 0 → AND 하면 하위 비트 날리고 8의 배수 됨)
비트 마스킹을 활용한 빠른 나머지 제거 방식 */
#define ALIGN(size) (((size) + (ALIGNMENT - 1)) & ~0x7)

/* size_t 크기를 8의 배수로 올린 값 (정렬 안전성 확보) */
/*정렬 깨지면 성능 문제 + 크래시 위험*/
#define SIZE_T_SIZE (ALIGN(sizeof(size_t)))


/* Basic constants and macros */
#define WSIZE 4 /* word size (byte)*/
#define DSIZE 8 /* double word SIZE (byte) */
#define CHUNKSIZE (1<<12) /* 초기 가용 블록과 힙 확장을 위한 크기 (byte)*/

#define MAX(x, y) ((x) > (y) ? (x) : (y)) // 둘중 더 큰 값을 반환하는 max 매크로

/*Pack a size and allocated bit into a word */
#define PACK(size, alloc) ((size) | (alloc)) /* 헤더, 푸터를 만들기 위한 매크로*/

/*Read and write a word at address p */
/*여기서 p는 메모리블록의 헤더 푸터를 가르킨다.*/
/*헤더와 푸터를 조작하는 매크로*/
#define GET(p)      (*(unsigned int *)(p)) // 해당 주소에 저장된 4바이트의 값을 읽어오는 것
#define PUT(p, val) (*(unsigned *)(p) = (val)) //해당 주소에 4바이트의 값을 써주는 것

/* Read the size and allocated fields from address p */
#define GET_SIZE(p)     (GET(p) & ~0x7) // 하위 3비트 무시 (사이즈만)
#define GET_ALLOC(p)    (GET(p) & 0X1) // 하위 1비트에 1을 넣고 and연산 (할당 여부만)

/* Given block ptr bp, compute address of its header and footer */
#define HDRP(bp)    ((char *)(bp) - WSIZE) // payload 기준 4바이트(1워드) 앞으로 가서 헤더 찾기
#define FTRP(bp)    ((char *)(bp) + GET_SIZE(HDRP(bp)) - DSIZE)// payload 기준 블록크기만큼 뒤로 가서 푸터 찾기

/* Given bloack 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)))
profile
똑똑해지고 싶어요

0개의 댓글