본문으로 건너뛰기
Practical RTOS Internals · 35/53

TLSF Allocator 분석 — Two-Level Segregated Fit O(1)

· Hawk · 8분 읽기

#한 줄 요약

“TLSF는 alloc과 free 모두 O(1)을 보장하는 bounded heap입니다.” — bitmap과 CLZ 명령 한 번이면 적합한 bucket을 즉시 찾습니다.

#어떤 문제를 푸는가

heap_4의 worst case는 free list 길이에 비례합니다. 평균은 빠르지만 bounded라고 말하기는 어렵습니다. RT 시스템에서 alloc이 control loop 안에 들어가면 수 µs ~ 수십 µs 변동이 곧 jitter로 이어집니다.

이 문제를 정면으로 푼 것이 TLSF입니다. Miguel Masmano 등이 2004년 발표한 알고리즘으로, block 크기에 무관하게 alloc·free·coalesce 모두 상수 시간을 보장합니다. 핵심 트릭은 bitmap과 CPU의 leading-zero-count 명령(ARM의 CLZ, x86의 LZCNT)을 결합해 적합한 free list bucket을 한 번의 비트 연산으로 찾는 것입니다.

채택 범위가 넓습니다. Linux PREEMPT_RT의 일부 경로, VxWorks memory partition, micro-ROS 기본 allocator, 그리고 AAA 게임의 frame allocator가 모두 TLSF나 그 변형을 씁니다. 임베디드 RT에서는 사실상의 표준 dynamic allocator입니다.

#구조 — 두 단계 Segregation

TLSF는 free block을 2차원 격자에 분류합니다.

Level 1 (first-level, FL) — power-of-2 bucket
FL[4] : 16 ~ 31 byte
FL[5] : 32 ~ 63 byte
FL[6] : 64 ~ 127 byte
...
FL[n] : 2^n ~ 2^(n+1)-1
Level 2 (second-level, SL) — 각 FL bucket을 2^M 등분 (보통 M=4 → 16등분)
FL[6] (64~127):
SL[0]=64-71 SL[1]=72-79 SL[2]=80-87 ... SL[15]=120-127

같은 FL bucket 안에서도 4-bit linear subdivision으로 더 정밀하게 분류합니다. M=4이면 *internal fragmentation 상한이 1/16 ≈ 6.25%*로 묶입니다.

각 SL bucket이 별도의 free list를 갖습니다. 그리고 어느 bucket에 free block이 있는지 없는지를 추적하는 두 단계 bitmap이 핵심 자료구조입니다.

전체 구조를 한 장으로 정리하면 이렇습니다. FL bitmap에서 set bit를 찾고, 해당 FL[i]의 SL bitmap에서 다시 set bit를 찾아 blocks[FL][SL] free list 머리에 도달합니다. CTZ 두 번에 적합 bucket이 결정됩니다.

TLSF two-level segregated buckets

typedef struct tlsf {
uint32_t fl_bitmap; /* FL bucket 점유 여부 — 32 bit */
uint32_t sl_bitmap[FL_MAX]; /* 각 FL의 SL bucket 점유 여부 */
block_t *blocks[FL_MAX][SL_MAX]; /* 실제 free list head */
} tlsf_t;

bit가 1이면 그 bucket에 free block이 적어도 하나 있다는 뜻입니다.

#Allocate — O(1)

요청 size로부터 FL, SL index를 계산하고, bitmap에서 그 이상의 첫 1을 찾아 해당 bucket head를 pop합니다. 모두 비트 연산입니다.

void *tlsf_malloc(tlsf_t *t, size_t size) {
/* 1) size → (fl, sl) mapping */
int fl = sizeof(size_t) * 8 - 1 - __builtin_clz(size);
int sl = (size >> (fl - SL_BITS)) & (SL_MAX - 1);
/* 2) round up — 다음 bucket */
if (++sl == SL_MAX) { fl++; sl = 0; }
/* 3) bitmap에서 fl 이상의 첫 1을 찾음 */
uint32_t sl_map = t->sl_bitmap[fl] & (~0U << sl);
if (sl_map == 0) {
uint32_t fl_map = t->fl_bitmap & (~0U << (fl + 1));
if (fl_map == 0) return NULL; /* OOM */
fl = __builtin_ctz(fl_map);
sl_map = t->sl_bitmap[fl];
}
sl = __builtin_ctz(sl_map);
/* 4) bucket head pop */
block_t *b = t->blocks[fl][sl];
t->blocks[fl][sl] = b->next_free;
if (!b->next_free) {
t->sl_bitmap[fl] &= ~(1U << sl);
if (!t->sl_bitmap[fl]) t->fl_bitmap &= ~(1U << fl);
}
/* 5) too large하면 split, 나머지를 다시 insert */
if (b->size >= size + MIN_BLOCK + HEADER) {
block_t *rest = block_split(b, size);
tlsf_insert(t, rest);
}
return block_to_ptr(b);
}

step 1~5 모두 고정 횟수의 명령입니다. free list 순회가 없습니다. block 수가 10개든 100만 개든 같은 시간에 끝납니다.

#ARM CLZ — Bucket 즉시 찾기

bitmap에서 첫 1의 위치를 찾는 것은 일반적으로 비싼 연산입니다. 그런데 ARM Cortex-M3 이상에는 CLZ 명령이 있습니다. 32-bit 값의 leading zero 개수를 1 cycle에 반환합니다. __builtin_clz__builtin_ctz가 이 명령으로 직접 컴파일됩니다.

clz r1, r0 ; r1 = leading zero count of r0
; MSB의 bit 위치를 즉시 얻음

TLSF가 원리적으로 O(1)이라는 것은 알고리즘 단계의 횟수가 상수라는 뜻입니다. 실용적으로 빠르다는 것은 그 단계가 모두 CLZ 같은 single-cycle 명령으로 구현된다는 것입니다. 두 조건이 모두 맞아야 RT에서 의미가 있습니다.

Cortex-M0 / M0+에는 CLZ가 없어서 software emulation을 써야 합니다. 이때는 TLSF의 우위가 약해집니다. M0급 타깃은 static + memory pool이 더 적합합니다.

#Free — O(1) + Coalesce

free에서 가장 비싼 부분은 인접 block과 합치는 작업입니다. TLSF는 block header에 물리적으로 직전 block을 가리키는 포인터(prev_phys_block)를 두어 주소 산술로 이웃을 즉시 찾습니다.

void tlsf_free(tlsf_t *t, void *ptr) {
block_t *b = ptr_to_block(ptr);
/* 1) 직전 block과 coalesce */
if (b->prev_phys && b->prev_phys->is_free) {
tlsf_remove(t, b->prev_phys);
b->prev_phys->size += b->size + HEADER;
b = b->prev_phys;
}
/* 2) 직후 block과 coalesce */
block_t *next = (block_t*)((uint8_t*)b + b->size);
if (next->is_free) {
tlsf_remove(t, next);
b->size += next->size + HEADER;
}
/* 3) 합쳐진 block을 다시 적절한 bucket에 insert */
tlsf_insert(t, b);
}

tlsf_removetlsf_insert해당 bucket의 free list 머리만 갱신하면 됩니다. doubly-linked list로 free list를 유지하면 *임의 block의 제거도 O(1)*입니다.

이 구조 덕분에 coalesce가 free의 평균 비용에 추가되지 않습니다. heap_4가 coalesce를 위해 free list를 주소순으로 유지해야 했던 부담이 TLSF에는 없습니다.

#단편화 상한

Masmano의 원 논문은 TLSF의 단편화 상한을 수학적으로 정리합니다.

  • Internal fragmentation1/2SL_BITS1 / 2^{\text{SL\_BITS}} (SL_BITS=4 → 6.25%)
  • Worst-case overhead ≤ 25% (대부분의 워크로드에서 실측)

heap_4의 단편화는 unbounded입니다. 워크로드가 나쁘면 가용 메모리의 90% 이상이 사용 불가 상태로 갈 수도 있습니다. TLSF는 수학적으로 보장된 상한을 갖는다는 점이 임베디드에서 큰 차이를 만듭니다.

#API 사용 — tlsf-bsd

mattconte/tlsf(MIT license, 약 2 KB 코드)가 가장 널리 쓰이는 구현입니다. 한 pool로 시작해 필요 시 region을 더할 수 있습니다.

#include "tlsf.h"
static uint8_t pool_memory[1024 * 1024]; /* 1 MB */
static tlsf_t tlsf;
void mem_init(void) {
tlsf = tlsf_create_with_pool(pool_memory, sizeof(pool_memory));
}
void *mem_alloc(size_t n) {
return tlsf_malloc(tlsf, n);
}
void mem_free(void *p) {
tlsf_free(tlsf, p);
}
/* 비연속 region 추가 — heap_5와 같은 효과 */
void mem_add_region(void *base, size_t size) {
tlsf_add_pool(tlsf, base, size);
}

tlsf_create_with_pool이 한 번에 자료구조를 초기화하고, 그 뒤에는 tlsf_malloc / tlsf_free만 호출하면 됩니다.

#성능 비교 — heap_4 vs TLSF

Cortex-M4 168 MHz, 32 KB heap, random size alloc/free 100회를 섞은 워크로드입니다.

Allocatoralloc avgalloc worstfree avgfree worst
heap_41.5 µs15 µs (가변)1.2 µs10 µs
TLSF0.8 µs1.5 µs (bounded)0.7 µs1.3 µs

평균은 두 배쯤 차이입니다. worst case는 10배 차이입니다. RT 시스템에서 의미 있는 차이는 평균이 아니라 worst입니다.

#micro-ROS와 자율주행

ROS 2의 micro-ROS는 RTOS 위에서 ROS 노드를 돌리는 런타임입니다. 기본 allocator를 외부에서 주입할 수 있어 TLSF allocator를 끼우는 것이 표준 권장입니다.

#include "rcl/allocator.h"
static rcl_allocator_t alloc = {
.allocate = tlsf_allocate_wrapper,
.deallocate = tlsf_deallocate_wrapper,
.reallocate = tlsf_reallocate_wrapper,
.zero_allocate = tlsf_calloc_wrapper,
.state = &tlsf_instance,
};

자율주행 ECU에서 PREEMPT_RT Linux + TLSF + ROS 2 조합이 점점 표준에 가까워지고 있습니다. bounded latency가 자율주행의 안전성과 직결되기 때문입니다.

#VxWorks Memory Partition

VxWorks는 partition 개념으로 TLSF와 유사한 격리를 제공합니다.

PART_ID part = memPartCreate(pool, size);
void *p = memPartAlloc(part, 100);
memPartFree(part, p);

partition마다 별도의 allocator 인스턴스가 돌고, 한 partition의 단편화가 다른 partition에 영향을 주지 않습니다. TLSF가 한 인스턴스 안에서 worst case를 보장한다면, partition은 서브시스템 사이 격리를 보장합니다. 두 기법은 보완 관계입니다.

#TLSF의 단점

만능은 아닙니다.

  • 메모리 overhead가 고정 1 KB 가량 필요합니다. bitmap과 bucket head 배열입니다.
  • 코드 크기가 약 2 KB입니다. heap_1보다는 큽니다.
  • 매우 작은 heap(<4 KB)에서는 overhead 비율이 50% 이상이 되어 의미가 없습니다.
  • 작은 alloc(16 byte 이하)은 minimum block size 때문에 낭비가 큽니다.

tiny embedded(STM32L0급)에서는 TLSF보다 static + memory pool 조합이 훨씬 효율적입니다. TLSF는 수십 KB 이상 heap을 다루는 mid-range 이상에서 진가를 발휘합니다.

#Linux SLUB와의 차이

Linux 커널의 SLUB allocator도 size class 기반이라는 점에서 TLSF와 닮았습니다. 하지만 SLUB는 per-CPU cacheNUMA-aware 분배가 핵심이고, single-thread WCET 보장은 목표가 아닙니다.

TLSF는 single-thread WCET를 수학적으로 보장하는 것이 목표입니다. multi-core에서는 별도 lock이 필요하며, 그 lock이 또 다른 latency 변수입니다. SMP RTOS에서는 per-core TLSF 인스턴스를 두는 패턴이 자주 쓰입니다.

#자주 보는 함정과 안티패턴

⚠️ 작은 pool에 TLSF 적용

2 KB pool에 TLSF를 올리면 overhead가 50%를 넘습니다. 수십 KB 이상에서만 의미가 있습니다. tiny system은 4-05의 memory pool이 답입니다.

⚠️ TLSF의 thread safety를 가정

mattconte/tlsf를 비롯한 대부분의 구현은 thread-unsafe입니다. 여러 task가 같은 인스턴스에 접근하면 mutex로 직접 wrap해야 합니다. 또는 per-thread 인스턴스를 둡니다.

⚠️ 같은 크기 반복 alloc/free에 TLSF

같은 크기를 계속 들고나면 coalesce가 일어날 일이 거의 없습니다. 이 워크로드는 memory pool이 훨씬 빠르고 단순합니다. TLSF는 다양한 크기가 섞일 때 진가를 발휘합니다.

⚠️ Cortex-M0에 TLSF 적용

CLZ 명령이 없어 software CLZ로 대체됩니다. 이러면 한 단계가 수십 cycle로 늘어나 O(1)의 실용적 의미가 약해집니다. M0급에서는 static + pool이 안전합니다.

#정리

  • TLSF는 alloc·free·coalesce 모두 O(1) bounded를 보장하는 dynamic allocator입니다.
  • 핵심 트릭은 two-level segregation + bitmap이며, ARM CLZ 명령으로 적합 bucket을 1 cycle에 찾습니다.
  • block header의 prev_phys_block으로 인접 block에 주소 산술로 접근하여 coalesce도 O(1)입니다.
  • 단편화 상한은 *internal 6.25%, worst-case overhead 25%*로 수학적으로 보장됩니다.
  • 자동차 ECU, 로봇 ROS 2, RT 게임, VxWorks 등 bounded latency가 필요한 거의 모든 영역에서 표준입니다.
  • 메모리·코드 overhead가 약 3 KB 필요하므로 tiny embedded에는 부적합합니다.
  • Cortex-M0급 또는 같은 크기 반복 워크로드는 memory pool이 더 적합합니다.

다음 편은 4-04 Static Allocation에서 동적 할당을 완전히 배제하는 패턴을 다룹니다.

#관련 항목

Practical RTOS Internals · 36 of 53

  1. 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
  2. 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
  3. 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
  4. 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
  5. 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
  6. 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
  7. 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
  8. 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
  9. 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
  10. 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
  11. 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
  12. 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
  13. 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
  14. 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
  15. 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
  16. 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
  17. 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
  18. 18RISC-V Context Switch 분석 — ECALL·mret·CSR
  19. 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
  20. 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
  21. 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
  22. 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
  23. 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
  24. 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
  25. 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
  26. 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
  27. 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
  28. 28Priority Ceiling Protocol — Immediate vs Original 비교
  29. 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
  30. 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
  31. 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
  32. 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
  33. 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
  34. 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
  35. 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
  36. 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
  37. 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
  38. 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
  39. 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
  40. 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
  41. 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
  42. 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
  43. 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
  44. 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
  45. 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
  46. 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
  47. 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
  48. 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
  49. 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
  50. 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
  51. 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
  52. 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
  53. 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL