CSAPP 9장 파트2: 빌려주고 돌려받다 보면 생기는 낭비

2026. 10. 1. 22:16ㆍ크래프톤 정글/포스팅

지금 위치: 파트 1이 "왜 allocator가 필요한가"였다면, 여기서는 "그 일이 왜 어려운가"를 다룹니다.
이번 파트의 질문: allocator는 무엇을 잘해야 하고, 무엇이 그것을 방해하며, 그래서 무엇을 결정해야 하는가?

1. allocator가 풀어야 하는 조건 (제약)

allocator는 아무렇게나 일할 수 없고, 아래 제약 안에서 일해야 합니다

제약 의미 이게 왜 어렵게 만드나
1. 임의의 요청 순서 할당/해제가 어떤 순서로 올지 모름
(짝이 맞거나 중첩된다는 보장도 없음)
미래를 예측해서 최적 배치를 짤 수 없다
2. 즉시 응답 요청을 모아서 한꺼번에 처리 불가 지금 당장 결정해야 한다
3. 힙만 사용 allocator가 쓰는 자료구조도 힙 안에 저장 관리 정보도 공간을 차지한다
4. 정렬 조건 지켜야함  어떤 타입도 담을 수 있게 정렬 패딩이 생긴다
5. 할당된 블록은 못 건드림 한번 준 블록은 수정·이동 불가 → compaction 불가 흩어진 조각을 모아서 정리할 수 없다

핵심은 마지막 둘입니다. "미래(요청 순서)를 모르고, 한번 준 건 옮길 수 없다"

이 때문에 아래 단편화가 생기면 되돌리기 어렵습니다.

2. allocator의 두 가지 목표, 그리고 충돌 

목표 1: Throughput (처리량) = 단위 시간당 처리하는 요청 수.
예를 들어 1초에 할당 500번 + 해제 500번을 처리하면 초당 1,000 operations입니다. 요청당 평균 시간을 줄이면 올라갑니다.

 

목표 2: Peak utilization (최대 메모리 활용도) = 애플리케이션이 실제로 쓴 데이터량 대비 힙이 얼마나 커졌나.

  • payload: 애플리케이션이 요청한 크기 (malloc(p)의 p바이트)
  • aggregate payload P_k: k번째 요청 이후 현재 할당된 블록들의 payload 합
  • H_k: 현재 힙 크기 (줄어들지 않고 커지기만 함)

말로 풀면 최대 메모리 활용도는 "지금까지 가장 많이 쓴 순간의 payload 합 ÷ 현재 힙 크기"입니다. 1에 가까울수록 힙이 알차게 쓰인 겁니다. 이 식을 외울 필요는 없고, "힙이 payload 대비 얼마나 부풀었나"라는 감각만 잡으시면 됩니다.

 

두가지 목표는 충돌합니다. 

  • 낭비를 줄이려면 빈 블록을 찾고, 쪼개고, 합치는 일을 더 해야 하고, 그만큼 시간이 듭니다.
  • 반대로 해제된 블록을 재사용하지 않고 힙 끝에서만 계속 잘라주면 가장 빠르지만, 힙이 금방 부풀어 utilization이 형편없어집니다.

따라서 allocator 설계의 궁극적인 목표는 이 둘 사이에서 균형을 잡는 것입니다.


3. 낭비의 정체: 단편화 (fragmentation)

utilization을 망치는 주범은 단편화, 즉 "쓸 수 있는 메모리가 있는데 요청에 쓰이지 못하는 상태"입니다.

내부 단편화와 외부 단편화 두 종류가 있습니다. 

내부 단편화 (internal fragmentation)

할당된 블록이 payload보다 큰 경우 블록 안쪽의 낭비입니다.

 

생기는 이유는 대표적으로 둘입니다.

  • 정렬을 맞추려고 크기를 올림 (5워드 요청 → 정렬 맞추려고 6워드 할당)
  • 블록에 최소 크기가 정해져 있음 (예: malloc(1)을 해도 16바이트 블록)
  • 헤더/푸터 같은 관리 정보도 payload가 아니므로 이 차이에 포함됨 

내부 단편화는 계산이 쉽습니다.

각 할당 블록의 (블록 크기 − payload)를 합치면 되고, 이전 요청 패턴과 allocator 구현만으로 정해집니다.

외부 단편화 (external fragmentation)

빈 공간의 합은 충분한데, 하나로 이어진 빈 블록이 요청보다 작은 경우입니다. 블록 바깥의 낭비입니다.

힙:  [A: 할당][ free 2 ][B: 할당][ free 2 ][C: 할당][ free 4 ]
                  ↑                  ↑
        빈 공간 합계 = 2+2+4 = 8워드
        하지만 8워드를 요청하면?  → 이어진 8워드가 없어서 할당을 못해줌

각각의 빈 공간 합은 8워드인데 3 블록에 나뉘어 있어서, 8워드 요청을 못 받아주고 sbrk로 힙을 늘려야 합니다.

 

외부 단편화는 계산이 어렵습니다. 같은 힙 상태라도 미래 요청에 따라 문제인지 아닌지가 달라지기 때문입니다.

  • 빈 블록이 전부 4워드이고, 앞으로 요청이 모두 4워드 이하라면 → 문제 없음
  • 앞으로 4워드보다 큰 요청이 하나라도 오면 → 외부 단편화

미래는 모르므로 정확히 측정도, 완벽히 예방도 못 합니다.

그래서 allocator는 "작은 빈 블록 여러 개보다 큰 빈 블록 몇 개를 유지하자"는 휴리스틱(어림 규칙)을 씁니다.

정리 

  내부 단편화 외부 단편화
낭비가 있는 곳 할당된 블록 안 블록들 사이의 빈 공간
원인 정렬, 최소 크기, 관리 정보, 큰 블록을 통째로 줌 요청·해제가 반복되며 빈 블록이 쪼개져 흩어짐
측정 쉬움 미래 요청에 의존해 어려움
줄이는 도구 분할(splitting) 배치 정책(placement), 병합(coalescing)

 

4. 그래서 allocator는 네 가지를 결정해야 한다

처리량(속도)을 높이고 활용도를 높이려면 allocator는 아래 네 가지를 결정해야 합니다.

 

단편화와 연결하면 각 결정이 왜 존재하는지가 보입니다.

결정 질문 어떤 문제를 겨냥하는가?
① 추적 (free block organization) 빈 블록을 어떻게 기록하고 찾지? 모든 결정의 토대. 속도(찾는 시간)에도 영향
② 배치 (placement) 요청이 오면 어느 빈 블록을 고르지? 외부 단편화, 속도
③ 분할 (splitting) 고른 블록이 너무 크면 남은 부분은? 내부 단편화
④ 병합 (coalescing) 방금 해제한 블록은 이웃과 합칠까? 외부 단편화

책은 배치·분할·병합은 여러 추적 방식에 공통으로 쓰이는 기법이므로 가장 단순한 추적 방식(implicit free list)을 가지고 설명합니다. 


allocator는 "요청 순서를 모르고, 한번 준 블록을 옮길 수 없다"는 제약 아래에서 
빠르게(throughput), 낭비 없이(peak utilization) 일해야 하는데, 둘은 충돌한다.
→ 낭비의 원인은 단편화이며, 블록 안쪽의 내부 단편화와 블록 사이의 외부 단편화가 있다. 
외부 단편화는 미래 요청에 따라 문제 여부가 달라져 완벽히 막을 수 없어서, 작은 빈 블록이 흩어지지 않게 하는 휴리스틱을 쓴다.
→ 이를 위해 allocator는 추적, 배치, 분할, 병합의 네 가지를 결정해야 한다.