2장: 할당 — 힙, 아레나, 풀, 버퍼
Part II — 모든 프로그램이 쓰는 어휘. 선행 장: 1장. 이 장을 마치면 반사적으로 malloc을 집어드는 대신 할당 전략을 의도적으로 고를 수 있고, 세 가지 비용 중 어느 것을 치르고 있는지 알게 된다.
이 장은 heap.h, arena.h, pool.h, allocator.h, buffer.h를 다룬다. 예전에 이 장의 7절이었던 스레드 안전성과 포인터 provenance 자료는 이제 나머지 동시성 주제와 함께 6장에 있다 — 책에서 가장 어려운 자료가 맨 앞쪽 장 중 하나에 들어앉아 있었기 때문이다.
목차
- 할당이 매개변수인 이유, 그리고 heap allocator
- Arena: 여러 객체, 하나의 수명
- Pool: 여러 객체, 하나의 크기
- Allocator trait
- 원시 바이트 버퍼
- 예제와 오용 사례
1. 할당이 매개변수인 이유, 그리고 heap allocator
malloc의 문제
malloc은 전역이다. 어떤 함수든 호출할 수 있고, 시그니처의 그 무엇도 함수가 호출하는지 아닌지를 말해 주지 않으며, 메모리가 무엇에 쓰이든 모든 호출은 똑같은 범용 allocator로 간다.
여기서 따라 나오는 결과가 넷이고, 아마 이미 겪어 봤을 것이다:
- 시그니처만 보고는 함수가 할당하는지 알 수 없다.
char *build_message(int n)은 할당할 수도, 정적 버퍼 안의 포인터를 반환할 수도, 리터럴을 반환할 수도 있다. 세 경우 모두 타입이 같으므로 호출자는 해제해야 하는지 알 수 없고, 답은 틀렸을지도 모르는 주석 속에 있다. - 프로그램의 한 부분만 전략을 바꿀 수 없다. 파서가 파싱이 끝날 때 전부 죽는 작은 할당을 만 번 한다면,
malloc과free는 만 번의 범용 할당과 만 번의 해제를 한다. bump allocator라면 한 번이면 된다. 모든 호출 지점을 다시 쓰지 않고는 그렇게 하겠다고 말할 방법이 없다. - 실패 경로를 테스트할 수 없다.
malloc을 원할 때 실패시키려면 전역으로 가로채야 한다 —LD_PRELOAD, 링커 트릭, 라이브러리 내부 호출까지 잡아채는#define malloc my_malloc. 그런데 정작 가장 테스트하고 싶은 분기는 메모리가 바닥났을 때 실행되는 그 분기다. - 힙이 없는 곳에서는 아예 쓸 수 없다. 펌웨어, 커널, 부트로더:
malloc이 없고, 그것을 전제하는 모든 라이브러리는 쓸 수 없다.
malloc은 잘못 설계된 것이 아니다. 그것은 고정된 정책이며, 다르게 하라고 말할 매개변수를 받지 않는다는 사실 때문에 모든 호출 지점에 하드코딩되어 있는 것이다.
이 라이브러리가 대신 하는 것
allocator는 값이고, 매개변수다. 할당할 수 있는 함수는 proven_allocator_t를 받고, 할당할 수 없는 함수는 받지 않는다. 발상은 그게 전부이고, 모든 결과가 여기서 따라 나온다:
proven_err_t proven_u8str_append(proven_u8str_t *str, proven_u8str_view_t data);
proven_err_t proven_u8str_append_grow(proven_allocator_t alloc, proven_u8str_t *str, proven_u8str_view_t data);시그니처를 읽어 보라. 첫 번째는 문자열을 키울 수 없으므로 텍스트가 들어가지 않으면 실패한다. 두 번째는 키울 수 있고, allocator를 받음으로써 그렇다고 말한다. 어느 쪽을 호출했는지 궁금해할 일이 결코 없다.
이제 같은 코드가 호출자가 고르는 세 가지 서로 다른 전략과 함께 동작한다:
| Allocator | 얻는 방법 | 개별 해제? | 쓸 때 |
| Heap | proven_heap_allocator() | 예 | 일반적인 경우. 서로 무관한 수명을 가진 객체들. |
| Arena | proven_arena_create(backing) 후 proven_arena_as_allocator(&a) | 아니오 — free는 no-op이다; 전체를 reset하거나 destroy한다 | 전부 같은 순간에 죽는 많은 할당: 요청 하나, 프레임 하나, 파싱 하나. |
| Pool | proven_pool_init(&p, base, size, align, bin_cap) 후 proven_pool_as_allocator(&p) | 예, free list로 들어간다 | 하나의 고정 크기인 객체를 반복해서 할당하고 해제할 때. |
힙(heap)에서 시작하라. 이유가 있을 때 나머지 둘로 손을 뻗되, 그 이유는 대개 측정이다.
Heap allocator(힙 할당자)
proven_allocator_t proven_heap_allocator(void);이것은 라이브러리의 인터페이스를 입은 malloc, realloc, free다. 그러지 않을 구체적인 이유가 없다면 이것을 써야 하며, 여기에 영리한 구석은 전혀 없다 — 그게 요점이다. 이 매뉴얼에서 달리 할 이유가 없는 모든 예제는 이것을 쓴다:
proven_allocator_t heap = proven_heap_allocator();
proven_result_u8str_t s = proven_u8str_create(heap, 64);
if (!proven_is_ok(s.err)) {
return; /* nothing was created, so there is nothing to destroy */
}
(void)proven_u8str_append(&s.value, PROVEN_LIT("ready"));
proven_u8str_destroy(heap, &s.value); /* the SAME allocator */값은 네 워드 — 컨텍스트 포인터 하나와 함수 포인터 셋 — 이고 값으로 전달된다. 복사는 공짜이고, 자신의 struct에 저장해도 되며, 파괴할 필요도 없다.
프리스탠딩(freestanding) 빌드에서는 proven_heap_allocator가 존재하지 않는다. 감쌀 malloc이 없기 때문이다. 이는 우회해야 할 제약이 아니라, 라이브러리 전체가 allocator를 매개변수로 받는 바로 그 이유이며, 정적 배열 위의 아레나(arena)가 같은 코드를 마이크로컨트롤러에서 돌아가게 하는 이유다. freestanding 모드를 참고하라.
잘못된 예 — 생성할 때와 다른 allocator로 파괴하기:
proven_result_u8str_t s = proven_u8str_create(arena_alloc, 64);
proven_u8str_destroy(heap_alloc, &s.value); /* wrong: heap free on arena memory */객체는 자신을 만든 allocator를 기억하지 않는다 — 그것이 객체를 작게 유지하는 요인이다. 오늘날 이것을 검사하는 것은 없으며, 실패는 다른 어딘가에서 나중에 드러나는 힙 손상이다. 구조적으로 짝을 지어라: allocator를 객체 옆에 두거나, 둘을 함께 전달하라.
2. Arena: 여러 객체, 하나의 수명
문제: 사망 시점이 같은 수많은 작은 할당
설정 파일을 파싱한다고 하자. 키마다 문자열 하나, 값마다 문자열 하나, 섹션마다 노드 하나를 할당한다 — 작은 할당 수천 개다. 그리고 파싱이 끝나면 결과를 만들고, 그 할당 하나하나는 모두 같은 순간에 쓰레기가 된다.
malloc으로는 그 대가를 두 번 치른다. 할당마다 free list를 검색하고, 장부를 갱신하고, 헤더가 붙은 메모리를 반환한다; free마다 메모리를 돌려주고 어쩌면 이웃과 병합한다. 그리고 하나도 빠뜨려선 안 된다. 잊어버린 free 하나면 누수이기 때문이다.
arena가 하는 관찰은 그 객체들이 전부 같은 수명을 가진다는 것이며, 따라서 개별 해제는 무의미한 작업이라는 것이다. 함께 죽는다면, 함께 해제될 수 있다.
Arena가 동작하는 방식
arena는 메모리 블록 하나와 오프셋 하나다. 할당한다는 것은 요청받은 정렬에 맞게 오프셋을 올림하고, 그 주소를 반환하고, 오프셋을 앞으로 옮기는 것이다. 알고리즘 전체가 이게 전부다:
[####used####| free ]
^ offset- 할당은 명령어 몇 개다. 검색도, free list도, 할당마다 붙는 헤더도 없다.
free는 아무 일도 하지 않는다. 의도적으로 no-op이다.- 회수는 arena 전체를 reset하거나 destroy해서 한다. 오프셋을 0으로 되돌리는 것이다. 연산 하나가 객체 만 개를 해제한다.
메모리는 여러분이 준다. proven_arena_create는 proven_mem_mut_t를 받는다 — 힙에서 받아 온 블록이든, static 배열이든, 스택 위의 영역이든 상관없다. arena는 스스로 할당하지 않으며, 그것이 밑에 힙이 없어도 동작하게 하는 요인이다.
포기하는 것
arena는 범용 allocator가 아니며, 그 맞바꿈은 실제적이다:
- 객체 하나를 해제할 수 없다. 수명이 실제로 공유되지 않는다면, arena는 설계상 누수한다 — 메모리는 reset에서만 회수된다.
- 바닥나는 것은 단단한 한계다. backing 블록은 고정되어 있다. 여기서의
PROVEN_ERR_NOMEM은 기계의 메모리가 바닥났다는 뜻이 아니라, 이 arena가 바닥났다는 뜻이다. - arena를 가리키는 모든 포인터는 reset에서 죽는다. 한꺼번에, 아무 경고도 없이. reset보다 오래 사는 뷰(view)는 dangling view다. 0장의 계약 2를 보라.
수명 공유가 프로그램에 대한 사실일 때 arena를 쓰라. 희망 사항일 때가 아니라.
proven_arena_t
typedef struct {
proven_mem_mut_t backing;
proven_size_t offset;
} proven_arena_t;필드:
backing: 호출자가 소유하는 가변 메모리 범위.offset:backing내에서 다음 할당 위치.
Arena 함수와 헬퍼
| API | 의도 | 매개변수 | 반환 |
proven_arena_create(backing) | backing 슬라이스 위에 arena를 초기화한다. | backing: 호출자 소유 메모리. | proven_arena_t. |
proven_arena_reset(arena) | arena의 모든 할당을 버린다. | arena. | void. |
proven_arena_destroy(arena) | 형식적 정리. | arena. | void; 호출자 backing arena에는 no-op. |
proven_arena_alloc_aligned(arena, size, align) | 명시적 정렬로 할당한다. | arena, 바이트 size, 2의 거듭제곱 align. | proven_result_mem_mut_t. |
proven_arena_realloc_aligned(arena, old_ptr, old_size, new_size, align) | 재할당; 꼬리(tail) 할당을 제자리에서 확장/축소할 수 있다. | 이전 할당 세부 정보와 새 크기. | proven_result_mem_mut_t. |
proven_arena_alloc(arena, size) | PROVEN_DEFAULT_ALIGNMENT로 할당한다. | arena, 바이트 size. | proven_result_mem_mut_t. |
proven_arena_alloc_aligned_or_panic(arena, size, align) | 할당하거나 패닉(panic) 훅을 호출한다. | arena, size, align. | proven_mem_mut_t. |
proven_arena_alloc_or_panic(arena, size) | 기본 정렬 panic 할당. | arena, size. | proven_mem_mut_t. |
proven_arena_as_allocator(arena) | arena를 proven_allocator_t로 노출한다. | arena. | allocator trait, 또는 널 arena에 대해 제로 allocator. |
Trait 어댑터 헬퍼:
proven_arena_alloc_traitproven_arena_realloc_traitproven_arena_free_trait
이들은 allocator 트레잇(trait)가 함수 포인터를 필요로 하기 때문에 노출된다. 애플리케이션 코드는 보통 대신 proven_arena_as_allocator()를 호출한다.
예제:
alignas(max_align_t) proven_byte_t storage[4096];
proven_arena_t arena = proven_arena_create((proven_mem_mut_t){
.ptr = storage,
.size = sizeof storage,
});
proven_result_mem_mut_t r = proven_arena_alloc(&arena, 64);
if (!proven_is_ok(r.err)) {
return; /* the arena cannot grow: it reports NOMEM instead */
}
proven_arena_reset(&arena); /* reclaims r and everything else at once */
proven_arena_destroy(&arena);성장 가능한 컨테이너와 함께 쓰는 arena
성장 가능한 컨테이너는 arena allocator를 사용할 수 있지만, arena의 free가 no-op이므로 성장이 이전 블록을 버릴 수 있다. 가능하면 용량을 미리 확보(reserve)하라.
올바른 예:
alignas(max_align_t) proven_byte_t storage[4096];
proven_arena_t arena = proven_arena_create((proven_mem_mut_t){
.ptr = storage,
.size = sizeof storage,
});
proven_allocator_t a = proven_arena_as_allocator(&arena);
/* Ask for the capacity up front, so growth never has to abandon a block. */
proven_result_array_t ar = PROVEN_ARRAY_INIT(a, int, 128);
if (!proven_is_ok(ar.err)) {
return;
}
(void)PROVEN_ARRAY_PUSH(&ar.value, int, 10);
PROVEN_ARRAY_DESTROY(&ar.value); /* correct, but arena free reclaims nothing */
proven_arena_reset(&arena); /* this is what gives the bytes back */잘못된 예 — 아주 작은 초기 용량에서 arena 안으로 성장시키기:
proven_result_array_t ar = PROVEN_ARRAY_INIT(a, int, 1);
for (int i = 0; i < 10000; ++i) {
PROVEN_ARRAY_PUSH(&ar.value, int, i); /* wrong: every regrow abandons the old block */
}재성장할 때마다 arena에 더 큰 블록을 요청하고 이전 것을 "해제"하는데 — arena에서 그것은 아무 일도 하지 않는다. 배열 자체는 올바르게 끝나지만, arena는 지금까지 할당한 모든 중간 크기를 그대로 쥔 채 끝난다: 원하던 버퍼 하나 위에 1, 2, 4, 8 … 8192개 원소어치의 버려진 공간이 얹힌다. 위의 올바른 예처럼 용량을 미리 확보하라.
잘못된 예 — reset보다 오래 사는 view:
proven_u8str_view_t name = /* ... built in the arena ... */;
proven_arena_reset(&arena);
use(name); /* wrong: those bytes are now free space */이것이 arena의 가장 날카로운 모서리다. reset은 회수하는 메모리를 건드리지 않으므로 바이트는 보통 아직 그대로 있고, 버그는 보통 테스트에서 드러나지 않는다 — 다음 할당이 그 위에 덮어쓰기 직전까지는.
3. Pool: 여러 객체, 하나의 크기
arena가 풀지 못하는 문제
arena는 수명이 공유된다고 가정한다. 정반대 모양을 한 작업 부하도 많다: 한 타입의 객체가 특별한 순서 없이 계속 만들어지고 파괴되는 경우다. 이벤트의 연결 리스트. 커졌다 작아졌다 하는 트리의 노드들. 생겼다 사라지는 연결 레코드들.
arena는 이것을 할 수 없다 — 객체 하나를 결코 회수하지 않으므로, 오래 도는 프로그램은 한없이 커질 것이다. 힙은 할 수 있고, 그것이 정확히 그 비용이다: 할당마다 검색하고, 해제마다 장부를 갱신하며, 범용 allocator는 이미 천 번이나 처리한 요청에 대해 범용적인 일을 한다.
풀(pool)이 하는 관찰은 이 객체들이 전부 같은 크기라는 것이다. 크기가 같다면, 해제된 것 하나가 앞으로의 요청에 정확히 들어맞으므로, 아무런 검색 없이 곧바로 다시 건네줄 수 있다.
Pool이 동작하는 방식
pool은 해제된 블록들의 작은 스택 — bin — 을 유지하고, 뻔한 일을 한다:
- 할당: bin에 뭔가 있으면 하나를 pop해서 반환한다. 포인터 읽기 하나와 감소 하나다. bin이 비어 있을 때만 하위 allocator로 간다.
- 해제: bin에 자리가 있으면 재사용을 위해 블록을 push한다. bin이 꽉 찼으면 pool이 메모리를 영원히 세워 두지 않도록 하위 allocator로 되돌려준다.
따라서 bin_cap은 다이얼이다: 이 pool이 예비로 쥐고 있어도 되는 메모리의 양이다.
포기하는 것
- 하나의 pool은 정확히 하나의 크기와 정렬만 처리한다. 그 밖의 요청은 다른 데서 처리되는 것이 아니라
PROVEN_ERR_INVALID_ARG로 거부된다. pool이 만들어질 때보다 더 엄격한 정렬도 거부되고, 더 느슨한 것은 블록이 이미 그것을 충족하므로 괜찮다. - pool은 살아 있는 객체를 추적하지 않는다.
proven_pool_destroy는 bin에 있는 것을 해제하지, 여러분이 아직 쥐고 있는 것을 해제하지 않는다. 할당한 것을 먼저 전부 해제하라. 그러지 않으면 누수인 데다가 이제 dangling이다.
arena 대 pool을 한 줄씩으로: arena는 *많이 할당하고 한 번에 전부 해제*를 위한 것이고, pool은 *같은 크기를 몇 번이고 저렴하게 할당하고 해제*하기 위한 것이다.
proven_pool_t
typedef struct {
proven_allocator_t base_alloc;
proven_size_t item_size;
proven_size_t item_align;
void **bin;
proven_size_t bin_cap;
proven_size_t bin_len;
} proven_pool_t;필드:
base_alloc: 새 블록과 재활용 bin에 사용되는 allocator.item_size: pool이 받아들이는 정확한 객체 크기. 그 외 크기는PROVEN_ERR_INVALID_ARG로 거부된다.item_align: 모든 블록이 할당되는 정렬. 더 엄격한 정렬 요청은PROVEN_ERR_INVALID_ARG로 거부되고, 더 느슨한 요청은 블록이 이미 그것을 충족하므로 처리된다.bin: 캐시된 free-list 배열.bin_cap: 최대 캐시 블록 개수.bin_len: 현재 캐시 블록 개수.
재활용이 작동하는 방식
pool은 하나의 큰 슬랩(slab)을 소유하지 않는다. 각 항목을 base_alloc에서 개별적으로 할당하고, 해제된 블록들의 작은 스택(bin, 최대 bin_cap개의 포인터로 이루어진 배열)을 유지한다:
- 할당.
bin_len > 0이면 맨 위 포인터를 pop한다(O(1),base_alloc호출 없음) — 이것이 pool의 전체 요점이다. 그렇지 않으면base_alloc로 넘어간다. - 해제.
bin_len < bin_cap이면 재사용을 위해 포인터를 bin에 push한다; 그렇지 않으면(bin이 꽉 참)base_alloc로 곧바로 되돌려 해제한다. 따라서 bin은 pool이 재사용을 위해 세워둘 수 있는 메모리의 양을 제한한다. - 파괴.
proven_pool_destroy는 bin에 아직 남아 있는 모든 포인터와 bin 배열 자체를 해제한다 — 하지만 살아 있는(넘겨준) 항목은 추적하지 않으므로, pool을 파괴하기 전에 할당한 모든 것을 해제해야 한다.
이는 pool을 지역(region) allocator가 아니라 수명이 짧은 동일 타입 객체(노드, 이벤트)를 위한 churn 최적화기(optimizer)로 만든다. "많이 할당하고 한 번에 전부 해제"하고 싶으면 arena를 사용하라; "같은 크기를 몇 번이고 저렴하게 할당/해제"하고 싶으면 pool을 사용하라.
반례
/* WRONG: the pool only handles its configured size/alignment. */
proven_allocator_t a = proven_pool_as_allocator(&pool); /* item_size == sizeof(Node) */
a.alloc_fn(a.ctx, sizeof(BigThing), alignof(BigThing)); /* not the pool's item -> rejected */
/* WRONG: destroying while items are still live leaks (and dangles) them. */
void *p = a.alloc_fn(a.ctx, sizeof(Node), alignof(Node)).value.ptr;
proven_pool_destroy(&pool); /* `p` is NOT freed and is now dangling */
/* RIGHT: free every handed-out item first, then destroy. */Pool 함수
| API | 의도 | 반환 |
proven_pool_init(pool, base_alloc, item_size, item_align, bin_cap) | 고정 크기 pool을 초기화한다. | PROVEN_OK 또는 에러. |
proven_pool_as_allocator(pool) | pool이 뒷받침하는 allocator trait를 반환한다. | proven_allocator_t. |
proven_pool_destroy(pool) | 캐시된 블록과 bin 저장소를 해제한다. | void. |
예제:
typedef struct Node { int value; } Node;
proven_pool_t pool = {0};
proven_err_t e = proven_pool_init(&pool, alloc, sizeof(Node), alignof(Node), 64);
if (!proven_is_ok(e)) {
return;
}
proven_allocator_t node_alloc = proven_pool_as_allocator(&pool);
proven_result_mem_mut_t n = node_alloc.alloc_fn(node_alloc.ctx, sizeof(Node), alignof(Node));
if (proven_is_ok(n.err)) {
/* Hand it back before destroy: the pool does not track live items. */
node_alloc.free_fn(node_alloc.ctx, n.value.ptr);
}
proven_pool_destroy(&pool);잘못된 예:
node_alloc.alloc_fn(node_alloc.ctx, sizeof(LargerObject), alignof(LargerObject));
/* wrong: one pool is for one fixed object size and alignment */4. Allocator 트레잇
이 절이 첫 번째가 아니라 네 번째인 이유
여러분은 세 가지 allocator를 그들이 공유하는 인터페이스를 보지 않고 이미 써 보았고, 그것은 의도적이었다. 인터페이스는 이 발상에서 가장 재미없는 부분이고 아무 맥락 없이 읽기에 가장 어려운 것이다: 함수 포인터 typedef 셋과 정렬 계약은, 그 뒤에 있는 heap과 arena와 pool을 보기 전까지는 거의 아무 의미도 없다.
이 절이 필요한 이유는 둘이다: 자신만의 allocator를 작성하는 것, 그리고 이 라이브러리의 모든 allocator가 지키기로 약속한 규칙을 이해하는 것. 위의 셋을 쓰기만 할 생각이라면 proven_heap_allocator(), proven_arena_as_allocator(), proven_pool_as_allocator()가 API 전부이니 건너뛰어도 된다.
여기서 trait란 인터페이스로 쓰이는 함수 포인터 struct를 말한다 — 손으로 써 낸, C 버전의 가상 테이블이다. proven_allocator_t는 라이브러리에서 가장 중요한 trait이고, stream.h와 random.h도 같은 모양을 쓴다.
함수 포인터 타입
typedef proven_result_mem_mut_t (*proven_alloc_fn_t)(
void *ctx,
proven_size_t size,
proven_size_t align
);
typedef proven_result_mem_mut_t (*proven_realloc_fn_t)(
void *ctx,
void *old_ptr,
proven_size_t old_size,
proven_size_t new_size,
proven_size_t align
);
typedef void (*proven_free_fn_t)(void *ctx, void *ptr);의도:
alloc_fn은 새로운 바이트 슬라이스를 할당한다.realloc_fn은 할당 크기를 변경하며 failure-atomic이어야 한다: 실패 시 이전 블록은 여전히 유효하고 변경되지 않으므로, 호출자는 아무것도 잃지 않는다.- 블록은 할당될 때와 같은
align으로 재할당되고 해제되어야 한다. allocator는 과도하게 정렬된(over-aligned) 요청에 대해 일반 요청과는 다른 하위 메커니즘을 선택할 수 있으며, heap allocator가 실제로 그렇게 한다:align <= alignof(max_align_t)— 이 라이브러리의 모든 문자열, 버퍼, 바이트 배열이 여기에 해당한다 — 는 제자리(in place)에서 성장이 일어날 수 있도록malloc/realloc을 거치고, 그보다 더 엄격하게 정렬된 것은 정렬된 allocator를 거친다. 블록을 다른 정렬 클래스로 되돌려주는 것은 정의되지 않은 동작이다. free_fn은 메모리를 해제한다. allocator가 크기 메타데이터를 필요로 한다면, 내부적으로 그것을 추적해야 한다.
proven_allocator_t
typedef struct {
void *ctx;
proven_alloc_fn_t alloc_fn;
proven_realloc_fn_t realloc_fn;
proven_free_fn_t free_fn;
} proven_allocator_t;필드:
ctx: allocator별 상태.alloc_fn: 할당 함수.realloc_fn: 재할당 함수.free_fn: 해제 함수.
레퍼런스
| 멤버 / API | 형태 | 계약 |
ctx | void * | allocator 자신의 상태. 호출자에게는 불투명하며, 첫 번째 인자로 되돌아온다. |
alloc_fn | proven_alloc_fn_t | align 정렬로 size 바이트를 할당한다. proven_result_mem_mut_t를 반환한다. |
realloc_fn | proven_realloc_fn_t | 크기를 바꾼다. failure-atomic: 실패 시 이전 블록은 손대지 않은 채 여전히 유효하다. |
free_fn | proven_free_fn_t | 해제한다. 블록이 할당될 때와 같은 align 클래스를 주어야 한다. |
proven_alloc_is_valid(alloc) | static inline bool | 모든 함수 포인터가 널이 아닐 때, 즉 trait를 호출할 수 있을 때 true. |
이 라이브러리의 모든 allocator가 지키며, 여러분의 것도 지켜야 하는 세 가지 규칙:
- 같은 allocator, 전체 수명 동안. 한 블록의 할당, 재할당, 해제는 하나의 allocator로 하라.
- 같은 정렬 클래스. 어떤 정렬로 할당된 블록은 그 정렬로 재할당되고 해제되어야 한다. allocator는 과도하게 정렬된 요청에 다른 메커니즘을 쓸 수 있다.
- 실패는 아무것도 바꾸지 않는다. 실패한
realloc_fn은 이전 블록을 유효하고 변경되지 않은 채로 남긴다.
static inline bool proven_alloc_is_valid(proven_allocator_t alloc);목적: 모든 함수 포인터가 널이 아닌지 확인한다.
반환: trait를 호출할 수 있으면 true.
올바른 예:
proven_allocator_t heap = proven_heap_allocator();
proven_err_t err = proven_alloc_is_valid(heap) ? PROVEN_OK : PROVEN_ERR_UNSUPPORTED;
(void)err; /* a freestanding build hands back a zero allocator, not a valid one */잘못된 예:
proven_allocator_t alloc = {0};
proven_result_mem_mut_t r = alloc.alloc_fn(alloc.ctx, 64, 8); /* wrong: null call */5. 원시 바이트 버퍼
무엇을 위한 것인가
proven_buf_t는 이 장에서 가장 단순한 것이다: 포인터 하나, 길이 하나, 용량 하나. 자기 바이트를 소유하고, 결코 성장하지 않으며, proven_u8str_t와 proven_u16str_t가 이것으로 만들어져 있다.
*텍스트*가 아니라 *바이트*를 원할 때 — 조립 중인 레코드, 소켓에 곧 써 넣을 프레임 — 그리고 최대 크기를 미리 알 때 직접 손을 뻗어라. 내용이 텍스트라면 대신 3장의 문자열 타입을 쓰라. 같은 저장소에 더해 NUL 종료, view, 검색, 포매팅까지 준다.
여기 있는 다른 모든 것과 마찬가지로 allocator를 저장하지 않으므로, proven_buf_destroy는 proven_buf_create에 주었던 것과 같은 것을 받는다.
proven_buf_t
typedef struct {
proven_byte_t *ptr;
proven_size_t len;
proven_size_t cap;
} proven_buf_t;필드:
ptr: 저장소 포인터.len: 현재 사용 중인 바이트.cap: 할당된 바이트.
proven_result_buf_t
typedef struct {
proven_err_t err;
proven_buf_t value;
} proven_result_buf_t;버퍼 함수
| API | 의도 | 반환 |
proven_buf_create(alloc, cap) | 용량이 0이 아닌 버퍼를 할당한다. | proven_result_buf_t; 유효하지 않은 allocator 또는 용량 0은 PROVEN_ERR_INVALID_ARG를 반환한다. |
proven_buf_append(buf, data) | 원시 바이트가 들어맞으면 추가한다. | PROVEN_OK 또는 에러. |
proven_buf_destroy(alloc, buf) | 일치하는 allocator로 버퍼 저장소를 해제한다. | void. |
예제:
proven_result_buf_t r = proven_buf_create(alloc, 64);
if (!proven_is_ok(r.err)) {
return;
}
proven_buf_t buf = r.value;
proven_err_t e = proven_buf_append(&buf, (proven_mem_view_t){
.ptr = (const proven_byte_t *)"abc",
.size = 3,
});
(void)e; /* PROVEN_ERR_OUT_OF_BOUNDS if it would not fit: the buffer never grows */
proven_buf_destroy(alloc, &buf);6. 예제와 오용 사례
파괴 시 allocator 일치시키기
올바른 예:
proven_allocator_t heap = proven_heap_allocator();
proven_result_buf_t r = proven_buf_create(heap, 128);
if (!proven_is_ok(r.err)) {
return;
}
proven_buf_destroy(heap, &r.value); /* the same allocator that created it */잘못된 예:
proven_result_buf_t r = proven_buf_create(heap, 128);
proven_buf_destroy(other_alloc, &r.value); /* wrong: allocator mismatch */Arena의 free는 no-op이다
alignas(max_align_t) proven_byte_t storage[1024];
proven_arena_t arena = proven_arena_create((proven_mem_mut_t){
.ptr = storage,
.size = sizeof storage,
});
proven_allocator_t a = proven_arena_as_allocator(&arena);
proven_result_mem_mut_t r = proven_arena_alloc(&arena, 32);
if (proven_is_ok(r.err)) {
/* Legal, and correct to write - but it intentionally reclaims nothing. */
a.free_fn(a.ctx, r.value.ptr);
}
proven_arena_reset(&arena); /* only this gives the 32 bytes back */Pool 크기 계약
올바른 예:
typedef struct Node { int value; } Node;
proven_pool_t pool = {0};
if (!proven_is_ok(proven_pool_init(&pool, alloc, sizeof(Node), alignof(Node), 8))) {
return;
}
proven_allocator_t node_alloc = proven_pool_as_allocator(&pool);
/* The pool serves exactly the size and alignment it was configured with. */
proven_result_mem_mut_t n = node_alloc.alloc_fn(node_alloc.ctx, sizeof(Node), alignof(Node));
if (proven_is_ok(n.err)) {
node_alloc.free_fn(node_alloc.ctx, n.value.ptr);
}
/* Anything else is refused with PROVEN_ERR_INVALID_ARG rather than served. */
proven_result_mem_mut_t wrong = node_alloc.alloc_fn(node_alloc.ctx, sizeof(Node) * 2, alignof(Node));
(void)wrong;
proven_pool_destroy(&pool);잘못된 예:
node_alloc.alloc_fn(node_alloc.ctx, sizeof(Other), alignof(Other));
/* wrong: PROVEN_ERR_INVALID_ARG - a pool is for one size and one alignment */버퍼 append는 성장하지 않는다
잘못된 예:
proven_buf_append(&buf, huge_data); /* returns out-of-bounds; no automatic growth */성장이 필요하면 proven_u8str_append_grow()나 배열을 사용하라.
버퍼 append는 겹침을 허용한다
올바른 예:
proven_result_buf_t r = proven_buf_create(alloc, 64);
if (!proven_is_ok(r.err)) {
return;
}
proven_buf_t buf = r.value;
proven_err_t e = proven_buf_append(&buf, (proven_mem_view_t){
.ptr = (const proven_byte_t *)"abcdefgh",
.size = 8,
});
if (proven_is_ok(e)) {
/* The source view points back into the buffer's own bytes. That is allowed:
* append moves rather than copies, so the overlap is well defined. */
e = proven_buf_append(&buf, (proven_mem_view_t){
.ptr = buf.ptr + 2,
.size = 4,
});
}
(void)e;
proven_buf_destroy(alloc, &buf);잘못된 예:
proven_sys_mem_copy(buf.ptr + buf.len, buf.ptr + 2, 4); /* copy is not the public contract here */실전 예제: 호출자가 제공한 메모리 위의 arena
테스트 스위트에서 컴파일되고 실행된다. arena를 손 뻗어 쓸 가치가 있게 만드는 bump-and-drop 수명을 보여준다: 할당은 거의 공짜이고, 개별적으로 해제되는 것은 없으며, proven_arena_reset이 전체 영역을 한 번에 회수한다.
/*
* 아레나는 기억을 소유하지 않는다. *여러분이* 가진 기억 위로 포인터를 밀고 갈 뿐이다.
* 거래는 그것이 전부다. 할당은 덧셈 한 번이고, 낱개 해제는 아예 없으며, reset 한 번에
* 전부 되돌려 받는다.
*
* 쓸 값어치가 나는 모양은 "밀고 나서 버리기"다. 한 단계 동안 마음껏 할당하고, 단계가
* 끝나면 reset 하나로 전부 회수한다. 물건마다 장부를 적을 일이 없으니 틀릴 일도 없고,
* 샐 것도 없다 - 아래의 뒷받침 저장소는 자동 수명을 가진 평범한 배열이다.
*/
int main(void) {
/* 뒷받침 저장소는 부르는 쪽의 것이다. 넉넉히 정렬해 두어, 첫 바이트부터도 어떤
* 정렬 요구든 아레나가 맞춰 줄 수 있게 한다. */
alignas(max_align_t) static proven_byte_t storage[4096];
proven_arena_t arena = proven_arena_create((proven_mem_mut_t){
.ptr = storage,
.size = sizeof storage,
});
/* --- 밀기 ------------------------------------------------------------- */
proven_result_mem_mut_t a = proven_arena_alloc(&arena, 64);
EXAMPLE_REQUIRE(proven_is_ok(a.err), "64 bytes must fit in a 4 KiB arena");
EXAMPLE_REQUIRE(a.value.ptr == storage, "the first allocation starts at the backing store");
/* 타입이 PROVEN_DEFAULT_ALIGNMENT 보다 큰 정렬을 요구하면 그것을 명시한다.
* 아레나는 거기까지 채워 넘어가므로, 건너뛴 바이트는 reset 전까지 그냥 사라진다. */
proven_result_mem_mut_t b = proven_arena_alloc_aligned(&arena, 32, 64);
EXAMPLE_REQUIRE(proven_is_ok(b.err), "an over-aligned block must still fit");
EXAMPLE_REQUIRE(((uintptr_t)b.value.ptr % 64) == 0, "the block must honour the requested alignment");
/* --- 다른 API 에 건네는 할당자로서의 아레나 --------------------------- */
/* proven 에서 proven_allocator_t 를 받는 것이면 무엇이든 아레나로 굴릴 수 있다.
* 그래서 아래 문자열은 `storage` 안에 산다. */
proven_allocator_t arena_alloc = proven_arena_as_allocator(&arena);
EXAMPLE_REQUIRE(proven_alloc_is_valid(arena_alloc), "the arena must expose a usable allocator");
proven_result_u8str_t s = proven_u8str_create(arena_alloc, 32);
EXAMPLE_REQUIRE(proven_is_ok(s.err), "the arena should be able to back a 32-byte string");
proven_err_t err = proven_u8str_append_grow(arena_alloc, &s.value, PROVEN_LIT("scratch line"));
EXAMPLE_REQUIRE(proven_is_ok(err), "appending into an arena-backed string must succeed");
/* 지우는 것은 여전히 옳고 소유 규칙이 요구하는 일이다 - 다만 아레나의 free 는
* 아무 일도 하지 않으므로 되돌리는 것이 없다. 이것은 누수가 아니다. 바이트는
* `storage` 의 것이고, 아래의 reset 이 그것을 돌려준다. */
proven_u8str_destroy(arena_alloc, &s.value);
proven_size_t used = arena.offset;
EXAMPLE_REQUIRE(used > 64, "every allocation above came out of the same backing store");
/* --- 버리기 ------------------------------------------------------------ */
/* 한 문장이 64바이트 블록과 정렬된 블록과 문자열을 모두 되돌린다. reset 의 값은
* 열 개를 할당했든 만 개를 할당했든 같다. */
proven_arena_reset(&arena);
EXAMPLE_REQUIRE(arena.offset == 0, "reset must reclaim every allocation at once");
/* 저장소가 정말 다시 쓰인다는 증거다. 다음 할당이 처음 자리로 돌아온다. reset
* 이전에 나눠 준 포인터는 이제 전부 매달린 포인터다 - reset 이 공짜인 값이다. */
proven_result_mem_mut_t c = proven_arena_alloc(&arena, 64);
EXAMPLE_REQUIRE(proven_is_ok(c.err), "allocation after reset must succeed");
EXAMPLE_REQUIRE(c.value.ptr == storage, "after reset the arena bumps from the beginning again");
/* --- 바닥나면 죽는 것이 아니라 오류다 ---------------------------------- */
proven_result_mem_mut_t too_big = proven_arena_alloc(&arena, sizeof storage);
EXAMPLE_REQUIRE(too_big.err == PROVEN_ERR_NOMEM, "an arena cannot grow: it reports NOMEM instead");
printf("arena: %zu bytes used before reset, %zu in use now\n",
(size_t)used, (size_t)arena.offset);
/* 형식상의 뒷정리. 부르는 쪽이 뒷받침하는 아레나에서는 하는 일이 없지만, 나중에
* 뒷받침 저장소가 힙이 되더라도 수명이 눈에 보이게 남는다. */
proven_arena_destroy(&arena);
return EXAMPLE_OK();
}실전 예제: 고정 크기 블록을 재활용하는 pool
테스트 스위트에서 컴파일되고 실행된다. 해제된 블록이 재활용 bin에서 곧장 다시 나오는 것과, pool이 무엇을 거부하는지를 보여준다.
/*
* 풀(pool)은 구역이 아니라 잦은 교체를 위한 최적화다. 타입 하나를 위한 것으로,
* 같은 크기의 블록을 몇 번이고 할당하고 해제하는 자리 - 리스트 노드, 이벤트, 파티클 -
* 에서 매번 malloc 값을 치르지 않게 한다.
*
* 풀은 해제된 블록을 담아 두는 작은 스택("bin")을 갖는다. 해제하면 바탕 할당자로
* 돌려보내는 대신 bin 에 얹고, 할당하면 거기서 하나를 꺼낸다. 둘 다 O(1) 이고 힙을
* 건드리지 않는다. 그 재활용이 존재 이유 전부이고, 아래 검사가 그것이 실제로
* 일어남을 보인다.
*
* 소유: 풀은 해제된 블록을 캐시하지만, 자기가 *내준* 블록은 추적하지 않는다. 가져간
* 블록은 destroy 전에 모두 돌려주어야 한다 - 풀은 자기가 모르는 것을 해제할 수 없다.
*/
typedef struct {
int id;
int score;
} node_t;
int main(void) {
proven_allocator_t heap = proven_heap_allocator();
EXAMPLE_REQUIRE(proven_alloc_is_valid(heap), "hosted builds have a heap allocator");
/* 풀은 bin 으로 감당 못 하는 블록을 위한 바탕 할당자를 받고, 자기가 다루는 그 한
* 타입의 정확한 크기와 정렬을 받는다. 마지막 인자는 재사용을 위해 세워 둘 해제된
* 블록의 최대 개수다. */
proven_pool_t pool = {0};
proven_err_t err = proven_pool_init(&pool, heap, sizeof(node_t), alignof(node_t), 4);
EXAMPLE_REQUIRE(proven_is_ok(err), "initializing a pool of node_t must succeed");
if (!proven_is_ok(err)) {
return 1;
}
proven_allocator_t nodes = proven_pool_as_allocator(&pool);
/* --- 첫 블록: bin 이 비어 있으니 힙에서 온다 --------------------------- */
proven_result_mem_mut_t first = nodes.alloc_fn(nodes.ctx, sizeof(node_t), alignof(node_t));
EXAMPLE_REQUIRE(proven_is_ok(first.err), "the pool must be able to serve its own item type");
if (!proven_is_ok(first.err)) {
proven_pool_destroy(&pool);
return 1;
}
node_t *n = (node_t *)first.value.ptr;
*n = (node_t){ .id = 1, .score = 100 };
void *first_addr = n;
/* --- 돌려주기: 힙이 아니라 bin 으로 들어간다 --------------------------- */
nodes.free_fn(nodes.ctx, n);
EXAMPLE_REQUIRE(pool.bin_len == 1, "a freed block is cached for reuse, not returned to the heap");
/* 여기서부터 `n` 은 매달린 포인터다. 그 바이트는 다시 풀의 것이다. */
/* --- 두 번째 블록: 방금 해제한 것이 곧바로 돌아온다 -------------------- */
proven_result_mem_mut_t second = nodes.alloc_fn(nodes.ctx, sizeof(node_t), alignof(node_t));
EXAMPLE_REQUIRE(proven_is_ok(second.err), "allocating from a non-empty bin must succeed");
EXAMPLE_REQUIRE(second.value.ptr == first_addr, "the recycled block is the one that was freed");
EXAMPLE_REQUIRE(pool.bin_len == 0, "taking it back out empties the bin");
/* 재활용된 기억은 0 으로 채워지지 *않는다* - 풀이 남겨 둔 그대로다.
* 갓 받은 malloc 에 하듯 모든 필드를 초기화할 것. */
node_t *m = (node_t *)second.value.ptr;
*m = (node_t){ .id = 2, .score = 50 };
EXAMPLE_REQUIRE(m->id == 2, "the recycled block is ours to overwrite");
/* --- 풀 하나는 크기 하나와 정렬 하나만 감당한다 ------------------------ */
/* 그 밖의 요청은 거부된다. 이것은 범용 할당자가 아니고, 크기가 다른 블록을 조용히
* 내주지도 않는다. 코드는 PROVEN_ERR_UNSUPPORTED - "내 일이 아니다" - 이지 INVALID_ARG
* 가 아니다. 후자였다면 "쓰레기를 건넸다"로 읽혀 여러분이 자기 코드에서 버그를 찾아
* 헤매게 만들었을 것이다. */
proven_result_mem_mut_t wrong = nodes.alloc_fn(nodes.ctx, sizeof(node_t) * 2, alignof(node_t));
EXAMPLE_REQUIRE(wrong.err == PROVEN_ERR_UNSUPPORTED, "the pool only serves its configured item size");
/* --- 지우기 전에 살아 있는 블록을 모두 돌려준다 ------------------------ */
/* proven_pool_destroy 는 bin 에 있는 것과 bin 자체를 해제한다. `m` 은 아직 나가
* 있으므로, 이 free 를 건너뛰면 그대로 누수가 된다. */
nodes.free_fn(nodes.ctx, m);
printf("pool: %zu block(s) cached for reuse at teardown\n", (size_t)pool.bin_len);
proven_pool_destroy(&pool);
return EXAMPLE_OK();
}실전 예제: 시동 시 할당, 제자리에서 늘리기, 할당자 감싸기
앞의 두 예제는 대부분의 코드가 쓰는 방식으로 arena와 pool을 쓴다. 이 예제는 프로그램이 실제 규모가 되었을 때 손이 가는 세 가지를 다룬다.
- 시동(start-up) 단계의 할당. 어떤 할당은 실패했을 때 갈 길이 없다. 프로그램이 자기 설정 표조차 얻지 못했다면 남은 할 일이 없다.
proven_arena_alloc_or_panic()과proven_arena_alloc_aligned_or_panic()은 result가 아니라 메모리 블록 자체를 돌려주고, 실패는 패닉 핸들러(panic handler, 회복 불가능한 실패가 났을 때 불리는 함수)로 넘긴다 (1장 6절). 그때 무슨 일이 일어날지는proven_set_panic_handler()로 고른다. 예제는 메시지를 기록하는 핸들러를 설치해 실패를 보여 준 뒤, 기본 핸들러를 되돌린다. - 마지막으로 받은 블록 늘리기.
proven_arena_realloc_aligned()는 그 블록을 있던 자리에서 늘린다. 가장 최근 블록이 곧 사용 영역의 끝에 있는 블록이기 때문이다. 그보다 앞선 블록은 끝으로 복사된다 — 여전히 옳지만, 더 이상 공짜는 아니다. - 할당자(allocator) 감싸기. 할당자는 함수 포인터 셋과 문맥 포인터 하나이므로, 호출을 세거나 기록하거나 일부러 열 번째 호출을 실패시키는 래퍼(wrapper, 감싸는 코드)는 전달 함수 세 개면 된다. arena 자신의 세 함수 —
proven_arena_alloc_trait(),proven_arena_realloc_trait(),proven_arena_free_trait()— 가 공개되어 있는 이유가 바로 이것이다. 여러분의 래퍼는 arena를 다시 구현하는 대신 이들을 부르면 된다. 그러면proven_allocator_t를 받는 라이브러리의 모든 것이, 그 사실을 모른 채 여러분의 래퍼를 지나간다.
/*
* 자기 기억을 소유하는 프로그램이 언젠가 반드시 하게 되는 세 가지, 그리고 그것을 하는
* 호출들.
*
* - 시작할 때 할당하기. 이때 기억이 바닥나는 것은 프로그램이 이어서 갈 수 있는
* 상황이 아니다. `_or_panic` 계열이 그것을 위한 것이고, 패닉 처리기를 다는 것이
* "이어서 갈 수 없다" 가 이 프로그램에서 무슨 뜻인지 정하는 방법이다 - 덫에
* 걸리는 대신 로그 한 줄과 종료 같은 것으로.
*
* - 마지막에 할당한 블록을, 복사 없이 늘리기. 아레나는 그것을 제자리에서 할 수
* 있다. 마지막에 할당된 블록이 곧 쓰인 구역의 끝에 앉아 있는 그 블록이기 때문이다.
*
* - 할당자를 계측하기 - 할당 횟수를 세거나, 시험에서 열 번째를 일부러 실패시키거나 -
* 재는 대상 코드는 건드리지 않고서. 여기서 할당자는 함수 포인터 셋과 문맥 포인터
* 하나이므로, 감싸는 일은 전달 함수 셋을 쓰는 일이다. 아레나 자신의 셋이 공개되어
* 있는 이유가 바로 이것이다 - 여러분의 껍데기가 그것으로 전달한다.
*/
/* --- 패닉 처리기는 무엇을 위한 것인가 ------------------------------------ */
static int g_panics = 0;
static char g_last_panic[128];
/* 패닉 처리기는 메시지를 받아 프로그램의 운명을 정한다. 기본 처리기는 즉시 덫에
* 걸리는데, 그것이 운영에서는 옳고 시험에서는 쓸모없다 - 그래서 이 처리기는 메시지를
* 적어 두고 돌아온다. 돌아오는 것은 패닉 경로를 *일부러 시험할 때만* 허용되고, 그때
* 패닉한 호출이 돌려준 기억 블록은 써서는 안 된다. */
static void record_panic(const char *msg) {
++g_panics;
snprintf(g_last_panic, sizeof g_last_panic, "%s", msg);
}
/* --- 자기를 지나가는 것을 세는 할당자 ------------------------------------- */
typedef struct {
proven_arena_t *arena;
proven_size_t live_bytes;
proven_size_t alloc_calls;
proven_size_t free_calls;
} counting_ctx_t;
/* 셋 각각이 proven_allocator_t 의 필드 하나에 대응한다. 각자 제 장부를 적은 뒤 아레나의
* 공개된 특성 함수로 전달하므로, 재는 대상이 되는 동작은 아레나를 다시 구현한 것이
* 아니라 정확히 아레나의 동작이다. */
static proven_result_mem_mut_t counting_alloc(void *ctx, proven_size_t size, proven_size_t align) {
counting_ctx_t *c = (counting_ctx_t *)ctx;
proven_result_mem_mut_t r = proven_arena_alloc_trait(c->arena, size, align);
if (proven_is_ok(r.err)) {
c->live_bytes += size;
++c->alloc_calls;
}
return r;
}
static proven_result_mem_mut_t counting_realloc(void *ctx, void *old_ptr, proven_size_t old_size,
proven_size_t new_size, proven_size_t align) {
counting_ctx_t *c = (counting_ctx_t *)ctx;
proven_result_mem_mut_t r = proven_arena_realloc_trait(c->arena, old_ptr, old_size, new_size, align);
if (proven_is_ok(r.err)) {
c->live_bytes = c->live_bytes - old_size + new_size;
}
return r;
}
static void counting_free(void *ctx, void *ptr) {
counting_ctx_t *c = (counting_ctx_t *)ctx;
++c->free_calls;
proven_arena_free_trait(c->arena, ptr); /* 아레나의 free 는 아무 일도 하지 않는다. 세는 것이 요점이다 */
}
int main(void) {
alignas(max_align_t) static proven_byte_t storage[1024];
proven_arena_t arena = proven_arena_create((proven_mem_mut_t){ .ptr = storage, .size = sizeof storage });
/* --- 1. 실패하면 안 되는 시작 시점 할당 -------------------------------- */
proven_set_panic_handler(record_panic);
/* 풀어 볼 result 가 없다. 이들은 블록을 곧바로 돌려준다. 부르는 쪽이 손쓸 수 있는
* 오류가 없기 때문이다. 차이는 그것이 전부다. */
proven_mem_mut_t table = proven_arena_alloc_or_panic(&arena, 256);
EXAMPLE_REQUIRE(table.ptr != NULL, "a 256-byte start-up allocation must succeed");
EXAMPLE_REQUIRE(g_panics == 0, "a successful allocation must not panic");
/* 기본 경계보다 큰 정렬이 필요한 타입을 위한 정렬 지정 꼴 - 여기서는 64바이트
* 캐시 줄이고, 그것이 흔한 이유다. */
proven_mem_mut_t cache_line = proven_arena_alloc_aligned_or_panic(&arena, 64, 64);
EXAMPLE_REQUIRE(((proven_uintptr_t)cache_line.ptr % 64) == 0,
"the block must start on the boundary that was asked for");
EXAMPLE_REQUIRE(g_panics == 0, "an over-aligned allocation that fits must not panic either");
/* 이제 일부러 실패하는 경우다. 아레나가 결코 담을 수 없는 크기를 청한다. 기록하는
* 처리기를 달아 두었으니 그것을 지켜볼 수 있다. 기본 처리기였다면 프로그램은 여기서
* 멈췄을 것이고, 그것이 기본 처리기의 목적이다. */
proven_mem_mut_t impossible = proven_arena_alloc_or_panic(&arena, sizeof storage * 2);
(void)impossible; /* 처리기가 돌아온 뒤, 이 블록은 아무 뜻도 없다 */
EXAMPLE_REQUIRE(g_panics == 1, "exhausting the arena through _or_panic must panic");
EXAMPLE_REQUIRE(g_last_panic[0] != '\0', "the handler receives a message naming the call");
printf("panic handler saw: %s\n", g_last_panic);
/* NULL 을 건네면 덫에 거는 기본 처리기가 되돌아온다. 시험용 처리기를 그대로 두면
* 진짜 실패가 조용한 오염으로 바뀐다. */
proven_set_panic_handler(NULL);
/* --- 2. 가장 최근 블록을 제자리에서 늘리기 ----------------------------- */
/* 머리말을 읽고 나서 본문이 짐작보다 길다는 것을 알게 된 파서는, 방금 받은 버퍼를
* 복사하는 것이 아니라 늘리고 싶어 한다. */
proven_result_mem_mut_t buf = proven_arena_alloc_aligned(&arena, 32, alignof(proven_u32));
EXAMPLE_REQUIRE(proven_is_ok(buf.err), "the initial 32-byte buffer must fit");
proven_size_t before = arena.offset;
proven_result_mem_mut_t grown = proven_arena_realloc_aligned(&arena, buf.value.ptr, 32, 96, alignof(proven_u32));
EXAMPLE_REQUIRE(proven_is_ok(grown.err), "growing the most recent block must succeed");
EXAMPLE_REQUIRE(grown.value.ptr == buf.value.ptr,
"the most recent block grows in place: same address, no copy");
EXAMPLE_REQUIRE(arena.offset == before + 64, "only the extra 64 bytes were taken");
/* 제자리 경로는 *마지막에* 할당된 블록에만 열려 있다. 다른 블록을 하나 더 받고 나면
* 앞의 것은 있던 자리에서 더 늘릴 수 없다 - 아레나는 대신 그것을 끝으로 복사하고,
* 옛 바이트는 다음 reset 까지 죽은 자리가 된다. 어느 쪽이든 옳다. 다만 공짜가 아닐
* 뿐이다. */
proven_result_mem_mut_t other = proven_arena_alloc(&arena, 16);
EXAMPLE_REQUIRE(proven_is_ok(other.err), "a second block must fit");
proven_result_mem_mut_t moved = proven_arena_realloc_aligned(&arena, grown.value.ptr, 96, 128, alignof(proven_u32));
EXAMPLE_REQUIRE(proven_is_ok(moved.err), "growing an older block must still succeed");
EXAMPLE_REQUIRE(moved.value.ptr != grown.value.ptr, "but it is relocated, not extended");
/* --- 3. 세는 껍데기 뒤의 아레나 --------------------------------------- */
proven_arena_reset(&arena);
counting_ctx_t counted = { .arena = &arena };
proven_allocator_t alloc = {
.ctx = &counted,
.alloc_fn = counting_alloc,
.realloc_fn = counting_realloc,
.free_fn = counting_free,
};
EXAMPLE_REQUIRE(proven_alloc_is_valid(alloc), "all three function pointers must be present");
/* 이제 할당자를 받는 라이브러리의 어느 부분이든 그 껍데기를 지나 돈다. 그것을 알지도
* 못한 채, 바뀐 것도 없이. */
proven_result_u8str_t s = proven_u8str_create(alloc, 16);
EXAMPLE_REQUIRE(proven_is_ok(s.err), "creating a string through the wrapper must succeed");
proven_err_t err = proven_u8str_append_grow(alloc, &s.value, PROVEN_LIT("a line long enough to need more room"));
EXAMPLE_REQUIRE(proven_is_ok(err), "appending past the initial capacity must succeed");
proven_u8str_destroy(alloc, &s.value);
EXAMPLE_REQUIRE(counted.alloc_calls >= 1, "the wrapper saw the string being created");
EXAMPLE_REQUIRE(counted.free_calls >= 1, "and saw it being destroyed");
printf("counting allocator: %zu alloc call(s), %zu free call(s), %zu bytes handed out\n",
(size_t)counted.alloc_calls, (size_t)counted.free_calls, (size_t)counted.live_bytes);
proven_arena_destroy(&arena);
return EXAMPLE_OK();
}반례 — 시험용 패닉 핸들러를 설치한 채 두는 경우:
proven_set_panic_handler(record_panic); /* returns instead of stopping */
/* ... the rest of the program ... */ /* wrong: no restore */돌아오는 핸들러는 이후의 모든 메모리 부족 패닉을, 널 블록을 돌려주고 계속 진행하는 호출로 바꾼다. 시험할 코드 주위에서만 설치하고, proven_set_panic_handler(NULL)로 기본값을 되돌린다.
반례 — 돌아온 핸들러 뒤에서 그 블록을 쓰는 경우:
proven_mem_mut_t m = proven_arena_alloc_or_panic(&arena, huge);
memset(m.ptr, 0, huge); /* wrong: m.ptr is null if the handler returned */반례 — 셋 중 하나를 빠뜨린 래퍼:
proven_allocator_t alloc = { .ctx = &counted, .alloc_fn = counting_alloc }; /* wrong */proven_alloc_is_valid()는 이것에 false를 돌려주고, 이 할당자를 통한 첫 realloc이나 destroy는 널 포인터를 호출한다. 둘은 그저 전달만 하더라도 셋을 모두 채운다.