Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
#한 줄 요약
“Intrusive = 객체 안에 list pointer를 박는 방식입니다.”
std::list처럼 wrapper node를 두지 않으므로 추가 할당이 0입니다.
#어떤 문제를 푸는가
std::list<T>의 내부 구조는 다음과 같습니다.
[head] → [Node{prev, next, T data}] → [Node{...}] → ...각 Node가 heap에 할당되고, T 외에 두 pointer만큼 메모리가 더 필요합니다. 임베디드에는 부담이 됩니다.
Intrusive container는 구조가 반대입니다.
[head] → [T{prev, next, other fields}] → [T{...}] → ...T 자체에 prev/next pointer가 들어 있어 list node를 별도로 할당하지 않습니다.
struct Task { Task* next; // intrusive link int priority; void (*work)(void);};
// List는 head pointer만Task* g_pending_head = nullptr;
void add_task(Task* t) { t->next = g_pending_head; g_pending_head = t;}추가 할당이 0입니다. Linux 커널과 FreeRTOS가 전부 이 패턴을 씁니다.
#Linux 커널 스타일 — list_head
Linux 커널의 list.h가 표준 idiom입니다.
struct list_head { list_head* prev; list_head* next;};
// 사용 — task에 list_head 임베드struct Task { list_head link; // 임베드 int priority; char name[16];};
list_head g_pending_list; // 헤드
void add_task(Task* t) { t->link.next = g_pending_list.next; t->link.prev = &g_pending_list; g_pending_list.next->prev = &t->link; g_pending_list.next = &t->link;}
// list_head에서 Task로 변환#define container_of(ptr, type, member) \ (type*)((char*)(ptr) - offsetof(type, member))
void iterate() { for (list_head* p = g_pending_list.next; p != &g_pending_list; p = p->next) { Task* t = container_of(p, Task, link); // t 사용 }}container_of 매크로가 핵심 트릭으로, list_head*에서 enclosing struct의 pointer를 계산합니다.
C++에서는 template으로 더 깔끔하게 쓸 수 있습니다.
#C++ Template intrusive list
template<typename T, typename T::ListLink T::*LinkMember>class IntrusiveList { T* head_ = nullptr; T* tail_ = nullptr;
public: void push_front(T* node) { if (!head_) { head_ = tail_ = node; (node->*LinkMember).next = nullptr; (node->*LinkMember).prev = nullptr; } else { (node->*LinkMember).next = head_; (node->*LinkMember).prev = nullptr; (head_->*LinkMember).prev = node; head_ = node; } }
void erase(T* node) { auto& link = node->*LinkMember; if (link.prev) (link.prev->*LinkMember).next = link.next; else head_ = link.next; if (link.next) (link.next->*LinkMember).prev = link.prev; else tail_ = link.prev; }
T* front() const { return head_; }
class Iterator { T* p_; public: Iterator(T* p) : p_(p) {} T& operator*() { return *p_; } Iterator& operator++() { p_ = (p_->*LinkMember).next; return *this; } bool operator!=(const Iterator& o) const { return p_ != o.p_; } };
Iterator begin() { return {head_}; } Iterator end() { return {nullptr}; }};사용:
struct Task { struct ListLink { Task *prev, *next; }; ListLink pending_link; ListLink ready_link; int priority;};
IntrusiveList<Task, &Task::pending_link> pending;IntrusiveList<Task, &Task::ready_link> ready;
Task t1, t2;pending.push_front(&t1);pending.push_front(&t2);
for (auto& t : pending) { process(t);}같은 Task가 서로 다른 link를 두면 두 list에 동시에 들어갈 수도 있습니다. 매우 유연한 구조입니다.
#Boost.Intrusive
Boost가 제공하는 intrusive container 라이브러리이며 임베디드 친화적입니다.
#include <boost/intrusive/list.hpp>
namespace bi = boost::intrusive;
struct Task : public bi::list_base_hook<> { int priority;};
using TaskList = bi::list<Task>;
TaskList g_pending;
Task t1, t2;g_pending.push_back(t1);g_pending.push_back(t2);
for (auto& t : g_pending) { process(t);}bi::list_base_hook이 prev/next를 포함하고, 상속만 하면 intrusive가 됩니다.
여러 list에 동시에 가입할 수도 있습니다.
struct PendingTag;struct ReadyTag;
struct Task : public bi::list_base_hook<bi::tag<PendingTag>>, public bi::list_base_hook<bi::tag<ReadyTag>> { int priority;};
using PendingList = bi::list<Task, bi::base_hook<bi::list_base_hook<bi::tag<PendingTag>>>>;using ReadyList = bi::list<Task, bi::base_hook<bi::list_base_hook<bi::tag<ReadyTag>>>>;장점은 다음과 같습니다.
- Heap 사용이 0입니다.
- 추가 메모리가 prev/next만큼만 늘어납니다.
- node 자체를 알고 있으면 erase가 O(1)입니다.
#Intrusive vs std::list 비교
구조의 차이를 한눈에 보면 다음과 같습니다.
std::list<T> | Intrusive | |
|---|---|---|
| 메모리 | T + Node (heap) | T 안에 link (free) |
| 할당 | 매번 heap alloc | 0 |
| Erase | O(n) (iterator로) | O(1) (node 자체로) |
| 다중 list 가입 | 어려움 (T를 복사) | 자연스러움 |
| Type safety | 강함 | 보통 (template으로 강화) |
| 표준 | C++11 | non-standard (Boost) |
임베디드에서는 intrusive가 압도적으로 유리합니다.
#ETL의 intrusive_list
#include <etl/intrusive_list.h>
class Task : public etl::list_link<0> {public: int priority;};
using TaskList = etl::intrusive_list<Task, etl::list_link<0>>;
TaskList tasks;Task t1, t2;tasks.push_back(t1);tasks.push_back(t2);ETL은 임베디드 친화적이며 Boost보다 가볍습니다.
#다른 intrusive 자료구조
#Intrusive doubly linked list (RB-tree)
#include <boost/intrusive/set.hpp>
struct OrderedTask : public bi::set_base_hook<> { int priority; bool operator<(const OrderedTask& o) const { return priority < o.priority; }};
using TaskSet = bi::set<OrderedTask>;TaskSet sorted_tasks;RB-tree로 priority가 자동 정렬되고 heap도 쓰지 않습니다.
#Intrusive hash map
struct Item : public bi::unordered_set_base_hook<> { int key; // ...};
using ItemMap = bi::unordered_set<Item>;bucket array는 stack이나 static으로 별도 할당합니다.
#임베디드 — RTOS Task Queue
전형적인 사용처는 RTOS scheduler의 task queue입니다.
struct TaskTcb { TaskTcb* ready_next; // intrusive ready queue TaskTcb* delayed_next; // intrusive delayed queue uint32_t stack_ptr; uint32_t state; uint32_t priority; uint32_t wakeup_time; void (*entry)(void*);};
static TaskTcb* g_ready_head[MAX_PRIORITY]; // priority별 queuestatic TaskTcb* g_delayed_head;
void task_make_ready(TaskTcb* t) { t->ready_next = g_ready_head[t->priority]; g_ready_head[t->priority] = t;}heap을 쓰지 않으면서 모든 연산이 O(1)입니다. FreeRTOS와 Zephyr 모두 이 패턴을 따릅니다.
#임베디드 — Event Pool
struct Event : public bi::list_base_hook<> { int type; uint32_t data;};
ObjectPool<Event, 64> event_pool;bi::list<Event> event_queue;
void post_event(int type, uint32_t data) { auto* e = event_pool.allocate(); if (e) { e->type = type; e->data = data; event_queue.push_back(*e); }}
void process_events() { while (!event_queue.empty()) { auto& e = event_queue.front(); event_queue.pop_front(); handle_event(e); event_pool.deallocate(&e); }}Pool에서 객체를 할당하고 intrusive list로 enqueue하는 방식이 임베디드 event-driven의 표준입니다.
#자주 보는 함정과 안티패턴
#1. Container의 소유권 모호
intrusive list는 node를 소유하지 않으므로 lifetime은 외부에서 관리해야 합니다. list가 소멸해도 node는 해제되지 않습니다.
{ Task t; list.push_back(t);} // t 소멸 — list에는 dangling pointerscope를 일치시키거나 명시적으로 erase합니다.
#2. Erase 이전 사용
auto* t = list.front();list.pop_front();t->priority; // OK (node는 살아 있음, list만 없앰)intrusive 컨테이너는 list와 node가 분리되어 있으므로 위 코드는 안전합니다. 다만 node를 free한 후 사용하면 UB입니다.
#3. Double linking
list1.push_back(t);list2.push_back(t); // 같은 link 사용 — 양쪽 깨짐서로 다른 link 멤버를 씁니다.
#4. 알맞은 link tag 없음
다중 list에 가입할 때는 각자 다른 tag가 필요합니다.
#5. Const correctness
intrusive container는 node 내부를 변경하므로 const list iteration에서도 non-const node 변경이 가능합니다. 주의가 필요합니다.
#6. Concurrent 수정
intrusive list의 push/pop은 atomic하지 않습니다. multi-task 환경에서는 mutex나 lock-free intrusive list가 필요합니다.
#측정 — std::list vs intrusive
같은 1000개 객체를 enqueue/dequeue한 결과입니다 (STM32F4).
std::list<Task>: push_back: ~150 cycles (heap alloc + node init) pop_front: ~80 cycles (heap free) total: ~230 ms (1000 ops × push/pop) heap usage: ~24 KB (Task + Node)
intrusive list<Task>: push_back: ~10 cycles (pointer update) pop_front: ~10 cycles total: ~20 ms heap usage: 0 (Task pool만)10배 이상 빠르고 heap도 쓰지 않아 임베디드에 적합한 결정적 동작을 보장합니다.
#정리
- Intrusive 컨테이너는 객체 안에 list link를 포함하므로 node 별도 할당이 0입니다.
- Linux 커널의
list_head와container_of가 전통적인 패턴입니다. - C++ 라이브러리로는 Boost.Intrusive와 ETL의 intrusive_list가 있습니다.
- 서로 다른 link를 두면 다중 list 가입이 자연스럽게 가능합니다.
- RTOS scheduler, event queue, driver chain에서 표준 도구로 쓰입니다.
- 컨테이너는 객체를 소유하지 않으므로 lifetime은 외부에서 관리해야 합니다.
#관련 항목
- Part 3-01: 동적 할당 없이
- Part 3-03: Pool Allocator — pool + intrusive
- Part 4-02: ETL 라이브러리
- Practical RTOS Internals — RTOS scheduler
#다음 글
Part 4-02: ETL 라이브러리 — 임베디드 STL 대체. heap 없는 vector, map, queue, fsm.
Embedded C++ for Real Systems · 30 of 41
- 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
- 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
- 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
- 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
- 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
- 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
- 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
- 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
- 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
- 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
- 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
- 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
- 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
- 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
- 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
- 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
- 17CRTP 패턴 분석 — vtable 없는 정적 다형성
- 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
- 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
- 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
- 21Custom Allocator 기초 — std::allocator 인터페이스 분석
- 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
- 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
- 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
- 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
- 26std::expected 분석 — C++23 결과 타입과 에러 전파
- 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
- 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
- 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
- 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
- 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
- 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
- 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
- 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
- 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
- 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
- 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
- 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
- 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
- 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
- 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
관련 글
임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
범용 HAL 구조 — 벤더 종속성 격리, 다중 보드/MCU 지원, 시리즈 마무리.
Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
UART, SPI, I2C — peripheral을 type-safe class로. Blocking, interrupt, DMA 패턴.
GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
GPIO pin = type — 컴파일 타임에 핀 설정 검증, runtime 비용 0.
이 글을 참조하는 글 (4)
- Lock-free Container 구현 — SPSC Queue·Ring Buffer— Embedded C++ for Real Systems
- ETL 라이브러리 분석 — Embedded Template Library의 STL 대체— Embedded C++ for Real Systems
- 임베디드 C++ 소유권 모델 — single·shared·borrow 패턴— Embedded C++ for Real Systems
- 동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀— Embedded C++ for Real Systems