Workspace IndexAlgorithms › Day 31

Memory Allocator Design — Ideas Behind jemalloc and mimalloc TODO

Algorithms · Day 31 / 100 · B. Compilers, Runtimes & VMs (Day 20-35)

Concept

A general-purpose memory allocator has to handle arbitrary-sized requests quickly while keeping fragmentation under control, so it rounds sizes into a few dozen size classes and manages same-class blocks together using a segregated free list structure. jemalloc assigns each thread an arena and adds a thread-local cache called tcache, so most allocations and frees complete without taking a lock, and it pulls memory from the OS in large units (chunks or extents) that it then carves up. mimalloc gives each thread its own heap and each page its own free list, and it separates local frees from remote frees (frees issued by a different thread) into distinct lists — the free list sharding idea — to cut down on atomic operations. What the two designs share is thread-local caching to eliminate contention, using size classes to turn external fragmentation into internal fragmentation so it stays manageable, and treating the moment memory is returned to the OS (purge/decay) as a policy decision.

In multithreaded servers, when throughput fails to scale with core count the culprit is often allocator contention, and RSS staying far above actual usage is likewise explained by the allocator's fragmentation and memory-return policy.

Code & Formula

# 메모리 할당자 설계 — jemalloc/mimalloc 아이디어: size class segregated free list + 스레드 로컬 캐시(tcache).
# 요청 크기를 몇 개의 size class 로 반올림해 같은 클래스끼리 free list 로 묶으면, 할당/해제가 O(1) 에 가까워진다.

SIZE_CLASSES = [8, 16, 32, 64, 128, 256]

def size_class_for(n):
    for c in SIZE_CLASSES:
        if n <= c:
            return c
    raise ValueError("too large for this toy allocator")

class TinyAllocator:
    def __init__(self):
        # 클래스별 free list (한 번 반환된 블록은 재사용) — jemalloc 의 segregated free list 흉내.
        self.free_lists = {c: [] for c in SIZE_CLASSES}
        self.next_addr = 0
        self.live = {}  # addr -> size_class (누가 뭘 들고 있는지 추적)

    def alloc(self, n):
        c = size_class_for(n)
        if self.free_lists[c]:
            addr = self.free_lists[c].pop()   # 스레드 로컬 캐시 hit 에 해당 — 락 없이 즉시 재사용
        else:
            addr = self.next_addr
            self.next_addr += c                # 새 청크는 size class 단위로만 늘어남(내부 단편화로 흡수)
        self.live[addr] = c
        return addr

    def free(self, addr):
        c = self.live.pop(addr)
        self.free_lists[c].append(addr)        # OS 에 즉시 반환하지 않고 재사용 대기열에 둔다(decay 정책 흉내)

alloc = TinyAllocator()
a = alloc.alloc(10)   # 10 -> class 16
b = alloc.alloc(60)   # 60 -> class 64
alloc.free(a)
c = alloc.alloc(15)   # 같은 class 16 free list 를 즉시 재사용 → 새 청크를 늘리지 않음

print("a addr:", a, "class:", alloc.live.get(a))
print("b addr:", b, "class:", alloc.live[b])
print("c addr:", c, "reused a's slot:", c == a)
print("free list state:", {k: v for k, v in alloc.free_lists.items() if v or k in (16, 64)})
print("total bytes carved from OS:", alloc.next_addr)

Exercise

Run the same multithreaded allocation benchmark while swapping in the default malloc versus jemalloc (or mimalloc) via LD_PRELOAD, and compare throughput and peak RSS.

Practical Connection

In services like a Go-based indexer or matching engine that create tens of thousands of small objects per second, the allocation pattern directly becomes GC pressure — the same principle explains why object reuse (sync.Pool) or pre-allocating slices pays off so much.

Where it lands in Jayverse

Key expressions

Words and phrases from this page worth keeping, with the Korean meaning and the sentence they come from.

Expression뜻 · 쓰이는 자리
keep ~ under control~을 통제 가능한 상태로 유지하다 · 단편화를 관리 가능한 수준으로 억제한다는 뜻 · "keeping fragmentation under control"
pull ~ from~로부터 (자원을) 끌어오다 · OS로부터 큰 단위로 메모리를 받아옴 · "it pulls memory from the OS in large units"
carve up(큰 덩어리를) 잘게 나누다 · 받아온 큰 메모리 청크를 잘라서 쓰는 것 · "chunks or extents) that it then carves up"
cut down on~을 줄이다 · 원자적 연산 횟수를 줄이기 위한 free list sharding · "to cut down on atomic operations"
fail to scale with~에 비례해 늘지 않다, ~만큼 확장되지 않다 · 코어 수를 늘려도 처리량이 안 느는 문제 · "throughput fails to scale with core count"
far above~보다 훨씬 위로, 크게 웃도는 · 실제 사용량보다 RSS가 과도하게 높은 상태 · "RSS staying far above actual usage"
treat ~ as~을 ~으로 취급하다, 간주하다 · 메모리 반납 시점을 정책적 결정으로 다룸 · "treating the moment memory is returned to the OS"
jemalloc제이말록(jemalloc) · 스레드별 아레나와 tcache를 쓰는 범용 메모리 할당자. "jemalloc assigns each thread an arena and adds a thread-local cache"
mimalloc미말록(mimalloc) · 스레드별 힙과 페이지별 프리리스트를 쓰는 메모리 할당자, free list sharding 기법의 예. "mimalloc gives each thread its own heap and each page its own free list"
RSS상주 집합 크기(Resident Set Size, RSS) · 프로세스가 실제 점유한 물리 메모리량, 할당자 정책과 괴리될 수 있음. "RSS staying far above actual usage"
LD_PRELOADLD_PRELOAD · 실행 전 공유 라이브러리를 끼워넣어 기본 malloc을 jemalloc 등으로 바꿔치기하는 리눅스 메커니즘. "swapping in the default malloc versus jemalloc (or mimalloc) via LD_PRELOAD"
tcache스레드 로컬 캐시(tcache) · jemalloc이 락 없이 할당·해제를 처리하도록 스레드마다 두는 캐시. "adds a thread-local cache called tcache"

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

메모리 할당자 설계 TODO

Algorithms · Day 31 / 100 · B. 컴파일러·런타임·VM (Day 20–35)

jemalloc·mimalloc의 아이디어

개념

범용 메모리 할당자는 임의 크기 요청을 빠르게 처리하면서 단편화를 억제해야 하는데, 이를 위해 크기를 몇십 개의 size class로 반올림해 같은 클래스끼리 모아 관리하는 segregated free list 구조를 쓴다. jemalloc은 스레드마다 arena를 배정하고 tcache라는 스레드 로컬 캐시를 두어 대부분의 할당·해제가 락 없이 끝나게 하며, 메모리는 큰 단위(chunk 또는 extent)로 OS에서 받아 잘라 쓴다. mimalloc은 스레드마다 heap을, 페이지마다 free list를 두고 로컬 해제와 원격(다른 스레드에서의) 해제를 분리된 리스트로 처리해 원자 연산을 줄이는 free list sharding 아이디어를 쓴다. 두 설계가 공유하는 핵심은 스레드 로컬 캐싱으로 경합을 없애고, size class로 외부 단편화를 내부 단편화로 바꿔 관리 가능하게 만들고, 해제된 메모리를 OS에 돌려주는 시점(purge/decay)을 정책으로 다루는 것이다.

멀티스레드 서버에서 처리량이 코어 수에 비례해 늘지 않을 때 원인이 할당자 경합인 경우가 흔하고, RSS가 실제 사용량보다 훨씬 크게 유지되는 현상도 할당자의 단편화·반환 정책으로 설명된다.

코드 · 수식

# 메모리 할당자 설계 — jemalloc/mimalloc 아이디어: size class segregated free list + 스레드 로컬 캐시(tcache).
# 요청 크기를 몇 개의 size class 로 반올림해 같은 클래스끼리 free list 로 묶으면, 할당/해제가 O(1) 에 가까워진다.

SIZE_CLASSES = [8, 16, 32, 64, 128, 256]

def size_class_for(n):
    for c in SIZE_CLASSES:
        if n <= c:
            return c
    raise ValueError("too large for this toy allocator")

class TinyAllocator:
    def __init__(self):
        # 클래스별 free list (한 번 반환된 블록은 재사용) — jemalloc 의 segregated free list 흉내.
        self.free_lists = {c: [] for c in SIZE_CLASSES}
        self.next_addr = 0
        self.live = {}  # addr -> size_class (누가 뭘 들고 있는지 추적)

    def alloc(self, n):
        c = size_class_for(n)
        if self.free_lists[c]:
            addr = self.free_lists[c].pop()   # 스레드 로컬 캐시 hit 에 해당 — 락 없이 즉시 재사용
        else:
            addr = self.next_addr
            self.next_addr += c                # 새 청크는 size class 단위로만 늘어남(내부 단편화로 흡수)
        self.live[addr] = c
        return addr

    def free(self, addr):
        c = self.live.pop(addr)
        self.free_lists[c].append(addr)        # OS 에 즉시 반환하지 않고 재사용 대기열에 둔다(decay 정책 흉내)

alloc = TinyAllocator()
a = alloc.alloc(10)   # 10 -> class 16
b = alloc.alloc(60)   # 60 -> class 64
alloc.free(a)
c = alloc.alloc(15)   # 같은 class 16 free list 를 즉시 재사용 → 새 청크를 늘리지 않음

print("a addr:", a, "class:", alloc.live.get(a))
print("b addr:", b, "class:", alloc.live[b])
print("c addr:", c, "reused a's slot:", c == a)
print("free list state:", {k: v for k, v in alloc.free_lists.items() if v or k in (16, 64)})
print("total bytes carved from OS:", alloc.next_addr)

연습

동일한 멀티스레드 할당 부하 벤치마크를 기본 malloc과 jemalloc(또는 mimalloc)을 LD_PRELOAD로 바꿔가며 돌려 처리량과 최대 RSS를 비교해 보라.

실무 · Verex 연결

Go로 짠 인덱서나 매칭 엔진처럼 초당 수만 건의 작은 객체를 만드는 서비스에서는 할당 패턴이 곧 GC 압력이므로, 객체 재사용(sync.Pool)이나 슬라이스 사전 할당이 왜 효과가 큰지 같은 원리로 설명된다.

Jayverse에서의 위치

핵심 표현

이 페이지의 영어 본문에서 배울 만한 단어와 표현, 뜻과 나온 자리.

Expression뜻 · 쓰이는 자리
keep ~ under control~을 통제 가능한 상태로 유지하다 · 단편화를 관리 가능한 수준으로 억제한다는 뜻 · "keeping fragmentation under control"
pull ~ from~로부터 (자원을) 끌어오다 · OS로부터 큰 단위로 메모리를 받아옴 · "it pulls memory from the OS in large units"
carve up(큰 덩어리를) 잘게 나누다 · 받아온 큰 메모리 청크를 잘라서 쓰는 것 · "chunks or extents) that it then carves up"
cut down on~을 줄이다 · 원자적 연산 횟수를 줄이기 위한 free list sharding · "to cut down on atomic operations"
fail to scale with~에 비례해 늘지 않다, ~만큼 확장되지 않다 · 코어 수를 늘려도 처리량이 안 느는 문제 · "throughput fails to scale with core count"
far above~보다 훨씬 위로, 크게 웃도는 · 실제 사용량보다 RSS가 과도하게 높은 상태 · "RSS staying far above actual usage"
treat ~ as~을 ~으로 취급하다, 간주하다 · 메모리 반납 시점을 정책적 결정으로 다룸 · "treating the moment memory is returned to the OS"
jemalloc제이말록(jemalloc) · 스레드별 아레나와 tcache를 쓰는 범용 메모리 할당자. "jemalloc assigns each thread an arena and adds a thread-local cache"
mimalloc미말록(mimalloc) · 스레드별 힙과 페이지별 프리리스트를 쓰는 메모리 할당자, free list sharding 기법의 예. "mimalloc gives each thread its own heap and each page its own free list"
RSS상주 집합 크기(Resident Set Size, RSS) · 프로세스가 실제 점유한 물리 메모리량, 할당자 정책과 괴리될 수 있음. "RSS staying far above actual usage"
LD_PRELOADLD_PRELOAD · 실행 전 공유 라이브러리를 끼워넣어 기본 malloc을 jemalloc 등으로 바꿔치기하는 리눅스 메커니즘. "swapping in the default malloc versus jemalloc (or mimalloc) via LD_PRELOAD"
tcache스레드 로컬 캐시(tcache) · jemalloc이 락 없이 할당·해제를 처리하도록 스레드마다 두는 캐시. "adds a thread-local cache called tcache"

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 1083. Rust 소유권·차용 검사기 내부(NLL)와 우회 패턴1085. FFI·ABI 경계와 안전성(패닉·정렬·수명) →