Maple Tree 자료구조

Maple Tree는 Linux 6.1에서 VMA 관리를 위해 Red-Black Tree를 대체한 현대적 B-tree 변형 자료구조입니다. 범위(range) 기반 키, RCU lock-free 읽기, 내장 잠금, 캐시 친화적 노드 배치를 통해 mmap 경쟁 상황에서 획기적인 성능 개선을 제공합니다. B-tree 노드 구조(pivot/slot), walk 알고리즘, RCU-safe 연산, 노드 분할/병합, VMA rbtree에서 maple tree로의 마이그레이션 과정, 그리고 실측 성능 비교까지 다룹니다.

전제 조건: Red-Black TreeXArray 문서를 먼저 읽으세요. Maple Tree는 XArray 코드베이스를 기반으로 구현되었으며, RB-Tree의 직접적인 후계자입니다.
일상 비유: Maple Tree는 페이지가 탭으로 나뉜 주소록과 비슷합니다. 각 탭(노드)에는 여러 주소 범위가 정렬되어 담겨 있고, 특정 주소를 찾을 때 탭을 넘기며 빠르게 좁혀 나갑니다. 한 노드에 여러 범위를 담아 메모리 접근 횟수를 줄이는 것이 핵심입니다. 기존 RB-Tree가 "한 페이지에 한 항목만 적힌 수첩"이라면, Maple Tree는 "한 페이지에 16개 항목이 정리된 다이어리"입니다.

핵심 요약

  • maple_tree — 최상위 트리 루트 구조체 (잠금 내장, RCU 보호)
  • ma_state — 순회/수정 작업의 커서 상태 (스택 위에 할당, 현재 위치·깊이·오프셋 추적)
  • maple_range_64 — 내부/잎 노드 타입 (최대 16개 범위 항목, pivot[15] + slot[16])
  • maple_arange_64 — Augmented 노드 (subtree 최대 gap 저장, VMA gap 검색에 특화)
  • pivot — 범위 경계 배열. pivot[i]는 slot[i]의 상한 값으로, B-tree의 키 역할을 합니다.
  • slot — 자식 노드 포인터(내부 노드) 또는 사용자 값 포인터(잎 노드)를 저장합니다.
  • mt_for_each — 전체 항목 순회 매크로 (RCU lock-free 읽기 지원)

단계별 이해

  1. 왜 RB-Tree를 대체했나
    기존 RB-Tree는 노드당 1개의 키만 저장해 캐시 미스가 잦았습니다. Maple Tree는 B-tree 방식으로 노드당 최대 16개 범위를 저장해 캐시 라인을 효율적으로 활용합니다.
  2. 범위 키(Range Key)
    각 항목은 단일 키 대신 [start, end] 범위로 저장됩니다. VMA에서 주소 범위가 자연스럽게 맵핑됩니다.
  3. pivot/slot 구조 이해
    내부 노드의 pivot[] 배열이 범위 경계를 정의하고, slot[] 배열이 자식 포인터를 담습니다. pivot[i]는 slot[i]가 담당하는 범위의 상한입니다.
  4. ma_state로 작업하기
    모든 삽입/삭제/검색은 ma_state라는 커서 구조체를 통해 이루어집니다. 스택에 할당되며 트리 내 현재 위치와 잠금 상태를 추적합니다.
  5. RCU 읽기 경로
    읽기 작업은 mt_find()를 RCU 보호 하에서 lock-free로 수행할 수 있습니다. 쓰기 측이 노드를 교체할 때 copy-on-write + RCU grace period를 사용합니다.
  6. VMA 관리에서의 역할
    Linux 6.1+에서 mm_structmm_rb(RB-Tree)가 mm_mt(Maple Tree)로 교체되어 mmap/munmap 경쟁 상황에서 성능이 크게 향상되었습니다.

개요 및 배경

Maple Tree는 Liam R. Howlett(Oracle)가 개발한 B-tree 변형 자료구조로, Linux 6.1(2022년 12월)에 병합되었습니다. 주된 목표는 VMA(Virtual Memory Area) 관리에서 Red-Black Tree의 한계를 극복하는 것이었습니다.

특성내용
병합 버전Linux 6.1 (2022년 12월)
개발자Liam R. Howlett (Oracle)
소스 위치lib/maple_tree.c, include/linux/maple_tree.h
기반 코드XArray (lib/xarray.c) 코드베이스에서 파생
노드 타입maple_range_64, maple_arange_64, maple_dense
최대 범위노드당 16개 항목 (64비트 시스템)
잠금 모델내장 spinlock + RCU lock-free 읽기
대체 대상mm_struct.mm_rb (RB-Tree) + mm_struct.mmap (연결 리스트)
이름의 유래: "Maple Tree"라는 이름은 개발자 Liam Howlett가 캐나다 출신이라 캐나다의 상징인 단풍나무(Maple)에서 따왔습니다. 코드에서 접두사 mt_, mas_, MA_를 사용합니다.

RB-Tree의 한계

문제RB-TreeMaple Tree
노드당 항목 수1개 키최대 16개 범위
캐시 라인 활용포인터 체이싱 (캐시 미스 빈번)노드 내 배열 순차 접근 (캐시 친화적)
잠금 모델외부 mm_write_lock (모든 쓰기에)내장 잠금 + RCU 읽기 (세밀한 잠금)
gap 검색별도 interval tree + augmented rb_treeaugmented 노드 내장 (arange_64)
VMA 순회RB-Tree + 별도 연결 리스트 필요단일 자료구조로 순회/검색/gap 모두 처리
메모리 오버헤드rb_node + vm_area_struct.vm_rb + list_head노드에 직접 범위와 값 저장
fork() 성능O(n) 복사 + mmap_sem 전체 잠금O(n) 복사 + 세밀한 잠금 (경합 40% 감소)
RB-Tree (Linux 6.0 이전) 0x5000 0x2000 0x8000 0x1000 0x3000 노드당 1개 키 5개 VMA = 5개 노드 높이 3, 캐시 미스 3회 + 별도 연결 리스트 필요 + augmented tree 별도 관리 Maple Tree (Linux 6.1+) maple_range_64 (잎 노드) p: 0x1FFF p: 0x3FFF p: 0x5FFF p: 0x8FFF ... VMA1 VMA2 VMA3 VMA4 ... pivot[15] + slot[16] = 캐시 라인 2개 gap[]: subtree 최대 빈 공간 (arange_64) 노드당 최대 16개 범위 5개 VMA = 1개 노드 높이 1, 캐시 미스 1회 순회 = pivot 배열 선형 스캔 gap 검색 내장 (augmented)
성능 데이터: Maple Tree 도입으로 fork()의 mmap_sem 경쟁이 최대 40% 감소하고, mmap/munmap 처리량이 멀티코어 환경에서 유의미하게 향상되었습니다 (Phoronix 벤치마크, Linux 6.1 기준). 특히 VMA 수가 많은 Java/Go 애플리케이션에서 효과가 두드러집니다.

자료구조 구성

struct maple_tree

/* include/linux/maple_tree.h */
struct maple_tree {
    union {
        spinlock_t      ma_lock;       /* 내장 스핀락 (쓰기 보호) */
        lockdep_map_p  ma_external_lock; /* 외부 잠금 사용 시 */
    };
    unsigned int        ma_flags;      /* MT_FLAGS_* 플래그 */
    void __rcu         *ma_root;      /* 루트 노드 포인터 (RCU protected) */
};

/* ma_flags 비트 필드 */
#define MT_FLAGS_ALLOC_RANGE   0x01  /* gap 검색 활성화 (arange_64 사용) */
#define MT_FLAGS_USE_RCU       0x02  /* RCU 보호 모드 */
#define MT_FLAGS_HEIGHT_OFFSET 0x02  /* 높이 정보 오프셋 */
#define MT_FLAGS_HEIGHT_MASK   0x7C  /* 높이 비트마스크 */
#define MT_FLAGS_LOCK_MASK     0x300 /* 잠금 타입 마스크 */
MT_FLAGS_ALLOC_RANGE: 이 플래그가 설정되면 트리가 maple_arange_64 노드를 사용하여 각 서브트리의 최대 gap 크기를 추적합니다. VMA 트리(mm_mt)는 항상 이 플래그로 초기화됩니다. 일반적인 범위 맵에서는 이 플래그 없이 maple_range_64만 사용합니다.

struct ma_state (커서)

struct ma_state {
    struct maple_tree *tree;    /* 대상 트리 */
    unsigned long      index;   /* 현재 검색/삽입 인덱스 (범위 시작) */
    unsigned long      last;    /* 범위의 끝 (범위 검색 시) */
    struct maple_enode *node;   /* 현재 노드 (인코딩된 포인터) */
    unsigned long      min;    /* 현재 노드 최소 범위 */
    unsigned long      max;    /* 현재 노드 최대 범위 */
    struct maple_alloc *alloc;  /* 미리 할당된 노드 풀 (분할용) */
    unsigned char      depth;  /* 현재 트리 깊이 */
    unsigned char      offset; /* 현재 노드 내 슬롯 오프셋 */
    unsigned char      mas_flags; /* 커서 상태 플래그 */
};

/* ma_state 초기화 매크로 — 스택에 할당 */
#define MA_STATE(name, mt, first, end) \
    struct ma_state name = { \
        .tree = mt, \
        .index = first, \
        .last = end, \
        .node = MAS_START, \
        .min = 0, \
        .max = ULONG_MAX, \
        .alloc = NULL, \
        .mas_flags = 0, \
    }
ma_state 필드용도비고
tree대상 Maple Tree 포인터초기화 시 설정
index검색/삽입 범위 시작결과 범위 시작으로 갱신됨
last검색/삽입 범위 끝결과 범위 끝으로 갱신됨
node현재 위치한 노드MAS_START, MAS_NONE, MAS_PAUSE 등 특수값
min / max현재 노드의 유효 범위walk 중 자동 갱신
alloc사전 할당된 노드 리스트분할 시 사용, mas_destroy()로 해제
depth현재 깊이루트=0부터 시작
offset현재 노드 내 슬롯 인덱스0..15 범위

노드 타입과 내부 구조

maple_range_64 일반 내부/잎 노드 (256 바이트) struct maple_pnode *parent (부모 포인터) unsigned long pivot[MAPLE_RANGE64_SLOTS - 1] (15개) void __rcu *slot[MAPLE_RANGE64_SLOTS] (16개) u8 pad[MAPLE_PAD] struct rcu_head rcu 총 크기: 256B = 캐시 라인 4개 (L1: 64B x 4) maple_arange_64 (Augmented) VMA gap 검색 특화 (256 바이트) struct maple_pnode *parent (부모 포인터) unsigned long pivot[MAPLE_ARANGE64_SLOTS - 1] (9개) void __rcu *slot[MAPLE_ARANGE64_SLOTS] (10개) unsigned long gap[MAPLE_ARANGE64_SLOTS] (10개) u8 meta.end / meta.gap struct rcu_head rcu gap[i] = slot[i] 서브트리의 최대 빈 공간 maple_dense (밀집 잎 노드) void __rcu *slot[MAPLE_NODE_SLOTS] (최대 64개) pivot 없음 — 인덱스 0..63의 연속 항목 고밀도 저장 노드 포인터 하위 비트에 노드 타입 인코딩: 00=dense, 01=range_64, 10=arange_64 maple_enode = void* | type_bits (XArray 패턴 동일)

pivot/slot 관계 상세

/* maple_range_64 노드의 pivot/slot 관계 */
/*
 * 예: 5개 VMA가 저장된 잎 노드
 *
 * pivot[0]=0x1FFF  pivot[1]=0x3FFF  pivot[2]=0x6FFF  pivot[3]=0x9FFF  pivot[4]=ULONG_MAX
 *   |                 |                 |                 |                 |
 * slot[0]=VMA1     slot[1]=VMA2     slot[2]=NULL      slot[3]=VMA3     slot[4]=VMA4
 *   |                 |                 |                 |                 |
 * [0, 0x1FFF]     [0x2000, 0x3FFF] [0x4000, 0x6FFF] [0x7000, 0x9FFF] [0xA000, ...]
 * 사용 중           사용 중           gap (빈 공간)     사용 중           사용 중
 *
 * slot[i]의 범위: [prev_pivot+1, pivot[i]]
 * slot[0]의 범위: [node_min, pivot[0]]
 * slot[i]가 NULL이면 해당 범위는 빈 공간 (gap)
 */

struct maple_range_64 {
    struct maple_pnode *parent;
    unsigned long pivot[MAPLE_RANGE64_SLOTS - 1]; /* 15개 경계값 */
    union {
        void __rcu *slot[MAPLE_RANGE64_SLOTS]; /* 16개 슬롯 */
        struct {
            void __rcu *pad[MAPLE_RANGE64_SLOTS - 1];
            struct rcu_head rcu;
        };
    };
};

/* maple_arange_64 — gap 필드 추가 */
struct maple_arange_64 {
    struct maple_pnode *parent;
    unsigned long pivot[MAPLE_ARANGE64_SLOTS - 1]; /* 9개 경계값 */
    void __rcu *slot[MAPLE_ARANGE64_SLOTS];      /* 10개 슬롯 */
    unsigned long gap[MAPLE_ARANGE64_SLOTS];       /* 10개 gap 값 */
    struct maple_metadata meta;                    /* 끝 슬롯, gap 인덱스 */
};
노드 타입pivot 수slot 수gap 수총 크기용도
maple_range_6415160256B일반 범위 트리
maple_arange_6491010256BVMA 트리 (gap 검색)
maple_dense0최대 640256B소수 연속 항목
arange_64의 트레이드오프: gap[] 배열을 추가하면 gap 검색이 O(log n)으로 가능하지만, pivot/slot 수가 15/16에서 9/10으로 줄어듭니다. 따라서 같은 수의 VMA를 저장하려면 트리 높이가 약간 더 높아질 수 있습니다. 그럼에도 gap 검색 성능 향상이 이 비용을 상쇄합니다.

Walk 알고리즘

Maple Tree의 핵심 연산은 walk입니다. 주어진 인덱스에 해당하는 잎 노드와 슬롯을 찾아가는 과정으로, B-tree의 top-down 탐색과 동일한 원리입니다.

루트 노드 (내부, arange_64) pivot: [0x3FFFF, 0x7FFFF, 0xBFFFF, ...] slot: [child0, child1, child2, ...] 검색: index=0x55000 pivot 배열 스캔 0x55000 > 0x3FFFF → slot[1] (child1) 선택 내부 노드 (child1) pivot: [0x4FFFF, 0x5FFFF, 0x6FFFF, ...] slot: [leaf0, leaf1, leaf2, ...] 0x55000 > 0x4FFFF, <= 0x5FFFF → slot[1] (leaf1) 잎 노드 (leaf1) pivot: [0x53FFF, 0x57FFF, 0x5BFFF, ...] slot: [VMA_A, VMA_B, VMA_C, ...] 0x55000 > 0x53FFF, <= 0x57FFF → VMA_B 반환!
/* lib/maple_tree.c — walk 핵심 로직 (간략화) */
static inline void *mas_walk(struct ma_state *mas)
{
    void *entry;

retry:
    entry = mas_state_walk(mas);
    if (mas_is_start(mas)) {
        /* 초기 상태: 루트에서 시작 */
        mas->node = mas_root(mas);
        mas->min = 0;
        mas->max = ULONG_MAX;
        mas->depth = 0;
        goto retry;
    }

    if (unlikely(mas_is_ptr(mas))) {
        /* 단일 항목 최적화: 루트가 직접 값을 가리킴 */
        if (mas->index == 0)
            return entry;
        return NULL;
    }

    /* 내부 노드 반복 순회 */
    while (!mas_is_leaf(mas)) {
        /* pivot 배열을 선형 스캔하여 적절한 슬롯 찾기 */
        mas_node_walk(mas);
        /* 자식 노드로 내려감 */
        mas->node = mas_get_slot(mas, mas->offset);
        mas->depth++;
    }

    /* 잎 노드에서 최종 슬롯 결정 */
    mas_node_walk(mas);
    return mas_get_slot(mas, mas->offset);
}

/* 노드 내 pivot 스캔 — 캐시 친화적 선형 탐색 */
static inline void mas_node_walk(struct ma_state *mas)
{
    unsigned long *pivots = ma_pivots(mas->node);
    unsigned char count = mt_pivot_count(mas->node);
    unsigned char offset = 0;

    /* pivot[i] >= index 인 첫 번째 i를 찾는다 */
    while (offset < count && pivots[offset] < mas->index)
        offset++;

    mas->offset = offset;
    /* min/max 범위 갱신 */
    mas->min = offset ? pivots[offset - 1] + 1 : mas->min;
    mas->max = offset < count ? pivots[offset] : mas->max;
}
선형 탐색 vs 이진 탐색: 노드 내 pivot 배열이 최대 15개(range_64)이므로 선형 탐색이 이진 탐색보다 빠릅니다. 이유는 선형 탐색이 순차적 메모리 접근으로 CPU 프리페치에 유리하고, 분기 예측 실패가 적기 때문입니다. 16개 항목의 선형 스캔은 캐시 라인 2개(128B)만 읽으면 됩니다.

핵심 API

초기화

/* 정적 초기화 */
DEFINE_MTREE(my_tree);

/* 동적 초기화 */
struct maple_tree mt;
mt_init(&mt);                          /* 기본 초기화 */
mt_init_flags(&mt, MT_FLAGS_ALLOC_RANGE); /* 범위 할당 모드 (gap 검색) */
mt_init_flags(&mt, MT_FLAGS_USE_RCU);     /* RCU 모드 (외부 잠금) */

/* 트리 해제 (모든 항목 제거 후 메모리 해제) */
mtree_destroy(&mt);

기본 CRUD 연산

/* 단일 인덱스 저장 */
int ret = mtree_store(&mt, index, entry, GFP_KERNEL);

/* 범위 저장 [first, last] */
ret = mtree_store_range(&mt, first, last, entry, GFP_KERNEL);

/* 조회 (RCU 보호, lock-free) */
void *val = mtree_load(&mt, index);

/* 삭제 */
void *old = mtree_erase(&mt, index);

/* 범위 내 첫 번째 항목 찾기 */
void *entry;
unsigned long idx = 0;
entry = mt_find(&mt, &idx, ULONG_MAX);
/* idx는 in/out: 검색 시작 인덱스 → 결과 인덱스 */

/* 삽입 (중복 키 거부) */
ret = mtree_insert(&mt, index, entry, GFP_KERNEL);
/* 이미 존재하면 -EEXIST 반환 */

ma_state 기반 고급 API

/* ma_state 초기화 (스택 할당) */
MA_STATE(mas, &mt, start_index, end_index);

/* 범위 기반 저장 */
mas_lock(&mas);
ret = mas_store_gfp(&mas, entry, GFP_KERNEL);
mas_unlock(&mas);

/* 순회 (잠금 없이 RCU 읽기) */
rcu_read_lock();
mas_for_each(&mas, entry, ULONG_MAX) {
    /* entry: 현재 값, mas.index/mas.last: 현재 범위 */
    do_something(entry, mas.index, mas.last);
}
rcu_read_unlock();

/* gap 검색: [min_gap] 크기 이상의 빈 공간 찾기 */
ret = mas_empty_area(&mas, min_addr, max_addr, min_gap);
/* 성공 시 mas.index에 빈 공간 시작 주소 */

/* 역방향 gap 검색: 높은 주소부터 */
ret = mas_empty_area_rev(&mas, min_addr, max_addr, min_gap);

ma_state 에러 처리

/* mas_store_gfp 실패 처리 — 표준 패턴 */
MA_STATE(mas, &mt, start, end);
int ret;

retry:
mas_lock(&mas);
ret = mas_store_gfp(&mas, entry, GFP_KERNEL);
mas_unlock(&mas);
if (mas_nomem(&mas, GFP_KERNEL))  /* 노드 사전 할당 후 재시도 */
    goto retry;

mas_destroy(&mas);  /* 미사용 사전 할당 노드 반환 */

if (mas_is_err(&mas))
    return xa_err(mas.node);  /* -ENOMEM, -EEXIST 등 */
코드 설명
  • mas_nomem()메모리 부족으로 실패했을 때 새 노드를 사전 할당하고 true를 반환합니다. 잠금 보유 상태에서 GFP_KERNEL 할당이 불가하므로, 내부적으로 잠금 해제 → 할당 → 재잠금 패턴을 자동으로 처리합니다.
  • mas_destroy()사전 할당했으나 사용되지 않은 노드 메모리를 반환합니다. 반드시 호출해야 메모리 누수가 없습니다.
  • mas_is_err()ma_state가 에러 상태인지 확인합니다. xa_err(mas.node)로 실제 에러 코드를 추출합니다 (XArray 에러 인코딩과 동일).

순회 매크로

/* mt_for_each: 전체 트리 순회 (가장 간단) */
unsigned long idx = 0;
void *entry;
mt_for_each(&mt, entry, idx, ULONG_MAX) {
    pr_info("idx=%lu entry=%p\n", idx, entry);
}

/* mas_for_each: ma_state로 순회 (범위 정보 접근 가능) */
MA_STATE(mas, &mt, 0, ULONG_MAX);
rcu_read_lock();
mas_for_each(&mas, entry, ULONG_MAX) {
    pr_info("[%lu, %lu] = %p\n", mas.index, mas.last, entry);
}
rcu_read_unlock();

/* mas_find / mas_next / mas_prev: 세밀한 순회 */
MA_STATE(mas, &mt, start, start);
rcu_read_lock();
entry = mas_find(&mas, end);     /* 첫 번째 항목 */
while (entry) {
    process(entry, mas.index, mas.last);
    entry = mas_next(&mas, end);  /* 다음 항목 */
}
rcu_read_unlock();

/* 역방향 순회 */
MA_STATE(mas, &mt, end, end);
entry = mas_find_rev(&mas, start);
while (entry) {
    process(entry, mas.index, mas.last);
    entry = mas_prev(&mas, start);
}

RCU-safe 연산

Maple Tree의 가장 강력한 특성은 쓰기 잠금 없이 읽기가 가능하다는 것입니다. 이는 RCU(Read-Copy-Update) 메커니즘을 통해 구현됩니다.

1. 기존 상태 부모 노드 구 노드 (A) Reader: rcu_dereference(slot[i])로 A 참조 2. Copy-on-Write 부모 노드 구 노드 (A) 신 노드 (B) Writer: 새 노드 B 할당, A 내용 복사 + 수정 3. rcu_assign_pointer 부모 노드 구 노드 (A) 신 노드 (B) 부모 slot[i] = B (원자적 포인터 교체) 4. Grace Period 후 구 노드 해제 기존 Reader가 A를 참조 중일 수 있음 call_rcu(&node_A->rcu, mt_free_node); 모든 Reader가 rcu_read_unlock() 완료 후 → 구 노드 A 안전하게 해제
/* 올바른 동시 접근 패턴 */

/* 읽기 측: 잠금 없이 안전 */
void *concurrent_read(struct maple_tree *mt, unsigned long key)
{
    return mtree_load(mt, key);  /* 내부적으로 RCU 처리 */
}

/* 쓰기 측: 내장 잠금 사용 */
int concurrent_write(struct maple_tree *mt,
                      unsigned long start, unsigned long end,
                      void *val)
{
    MA_STATE(mas, mt, start, end);
    int ret;
retry:
    mas_lock(&mas);
    ret = mas_store_gfp(&mas, val, GFP_KERNEL);
    mas_unlock(&mas);
    if (mas_nomem(&mas, GFP_KERNEL))
        goto retry;
    mas_destroy(&mas);
    return mas_is_err(&mas) ? xa_err(mas.node) : 0;
}
연산잠금 방식함수 접두사비고
읽기 (단일)잠금 불필요 (RCU 내장)mtree_load(), mt_find()내부 rcu_read_lock 자동
읽기 (순회)rcu_read_lock 권장mt_for_each(), mas_for_each()순회 중 일관성 보장
쓰기 (내장 잠금)내부 스핀락 자동 획득mtree_store(), mtree_erase()간편하지만 세밀도 낮음
쓰기 (ma_state)mas_lock()/mas_unlock()mas_store_gfp()재시도 패턴 필요
쓰기 (외부 잠금)호출자가 잠금 보유mas_store(), mas_erase()VMA: mmap_write_lock

노드 분할과 병합

Maple Tree는 B-tree 규칙에 따라 노드 용량을 관리합니다. 삽입 시 노드가 가득 차면 분할(split)하고, 삭제 시 노드가 희소해지면 병합(coalesce)합니다.

노드 분할 (Split) 과정 원래 노드 (가득 참: 16/16 슬롯 사용) [p0|p1|p2|p3|p4|p5|p6|p7|p8|p9|pA|pB|pC|pD|pE] = 15 pivots 새 항목 삽입 → 공간 부족 → Split! 좌측 노드 (8 슬롯) [p0|p1|p2|p3|p4|p5|p6] = 전반부 우측 노드 (8 슬롯 + 새 항목) [p8|p9|pA|pB|pC|pD|pE|NEW] = 후반부 부모 노드에 새 pivot 삽입 부모도 가득 차면 연쇄 분할 (cascading split)
이벤트동작노드 할당비고
삽입 - 여유 있음현재 노드에 항목 추가없음pivot 조정만
삽입 - 가득 참노드 분할 후 부모에 전파새 노드 1~2개연쇄 가능
삭제 - 충분항목 제거, pivot 재계산없음단순 작업
삭제 - 희소인접 노드와 병합없음 (해제)높이 감소 가능
범위 확대pivot 값만 갱신없음노드 이동 없음
범위 축소pivot 값 갱신, gap 재계산없음arange_64: gap 갱신
/* lib/maple_tree.c — 노드 분할 핵심 (간략화) */
static int mas_split(struct ma_state *mas,
                      struct maple_big_node *b_node)
{
    struct maple_enode *old = mas->node;
    struct maple_enode *left, *right;
    unsigned char split;

    /* 1. 분할 지점 결정 (대략 중간) */
    split = mas_split_final_node(b_node, mas, &mid_split);

    /* 2. 새 노드 2개 할당 */
    left  = mt_alloc_one(GFP_NOWAIT | __GFP_NOWARN);
    right = mt_alloc_one(GFP_NOWAIT | __GFP_NOWARN);

    /* 3. b_node 데이터를 좌/우로 분배 */
    mab_mas_cp(b_node, 0, split, left, ...);
    mab_mas_cp(b_node, split + 1, b_node->b_end, right, ...);

    /* 4. 부모 노드에 새 pivot과 포인터 삽입 */
    mas_set_parent(mas, left, mas->node, slot);
    mas_set_parent(mas, right, mas->node, slot + 1);

    /* 5. 구 노드 RCU 해제 예약 */
    call_rcu(&old->rcu, mt_free_rcu);

    return 0;
}
GFP(Get Free Pages) 플래그와 노드 분할: 노드 분할은 새 노드를 할당해야 합니다. mas_store_gfp(GFP_KERNEL)은 슬립을 허용하므로 분할 중 메모리 할당이 가능합니다. 원자적 컨텍스트에서는 미리 mas_preallocate()로 노드를 확보해야 합니다.

VMA 관리에서의 활용

mm_struct 변경 (Linux 6.1)

/* Linux 6.0 이전: RB-Tree 기반 VMA */
struct mm_struct {
    struct rb_root         mm_rb;      /* VMA RB-Tree 루트 */
    struct vm_area_struct  *mmap;      /* VMA 연결 리스트 (순회용) */
    unsigned long          highest_vm_end; /* 별도 augmented 정보 */
    int                    map_count; /* VMA 수 */
    /* ... */
};

/* Linux 6.1+: Maple Tree 기반 VMA */
struct mm_struct {
    struct maple_tree      mm_mt;      /* VMA Maple Tree */
    /* mm_rb, mmap 필드 제거됨 */
    /* highest_vm_end 제거 → arange_64 gap으로 대체 */
    int                    map_count; /* VMA 수 */
    /* ... */
};

/* mm_struct 초기화 시 Maple Tree 설정 */
static inline void mm_init(struct mm_struct *mm)
{
    mt_init_flags(&mm->mm_mt,
                  MT_FLAGS_ALLOC_RANGE | MT_FLAGS_LOCK_EXTERN |
                  MT_FLAGS_USE_RCU);
    /* ALLOC_RANGE: gap 검색 활성화 (arange_64 사용) */
    /* LOCK_EXTERN: mmap_lock으로 외부 잠금 */
    /* USE_RCU: RCU 보호 모드 */
}

VMA 검색 내부

/* mm/mmap.c: find_vma() — Linux 6.1+ */
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
    struct vm_area_struct *vma;

    /* 1. VMA 캐시 먼저 확인 (per-thread, 대부분 적중) */
    vma = vmacache_find(mm, addr);
    if (likely(vma))
        return vma;

    /* 2. Maple Tree에서 검색 */
    MA_STATE(mas, &mm->mm_mt, addr, addr);
    vma = mas_find(&mas, ULONG_MAX);

    /* 3. VMA 캐시 갱신 */
    if (vma)
        vmacache_update(addr, vma);

    return vma;
}

/* mmap gap 검색: 빈 주소 공간 찾기 */
static unsigned long unmapped_area(struct vm_unmapped_area_info *info)
{
    struct mm_struct *mm = current->mm;
    MA_STATE(mas, &mm->mm_mt, 0, 0);

    /* arange_64 노드의 gap 필드를 이용한 O(log n) 검색 */
    if (info->flags & VM_UNMAPPED_AREA_TOPDOWN)
        ret = mas_empty_area_rev(&mas, info->low_limit,
                                  info->high_limit, info->length);
    else
        ret = mas_empty_area(&mas, info->low_limit,
                              info->high_limit, info->length);

    return mas.index;  /* 사용 가능한 시작 주소 */
}
루트 (arange_64) gap: [0x2000, 0x10000, 0x500] 요청: 0x8000 크기 gap 찾기 gap[1]=0x10000 >= 0x8000 child0 gap=0x2000 (부족, 건너뜀) child1 (내부) gap: [0x800, 0x10000, 0x3000] gap[1]=0x10000 >= 0x8000 잎 노드 [VMA_A, NULL, VMA_B, ...] ← NULL = 0x10000 gap mas.index = gap 시작 주소 반환! 총 탐색: 3 노드 (루트→내부→잎) 시간 복잡도: O(log n) RB-Tree: 전체 순회 필요 → O(n)
gap 검색이 중요한 이유: mmap(NULL, ...) 호출 시 커널은 요청 크기만큼의 빈 가상 주소 공간을 찾아야 합니다. RB-Tree에서는 augmented interval tree를 별도로 유지해야 했지만, Maple Tree의 arange_64는 gap 정보가 노드에 내장되어 O(log n)으로 즉시 찾을 수 있습니다.

RB-Tree에서 마이그레이션

Linux 6.1에서 VMA 관리가 RB-Tree에서 Maple Tree로 전환되었습니다. 이 과정에서 수백 개의 파일이 수정되었으며, API 변환이 필요했습니다.

API 대응 표

RB-Tree APIMaple Tree 대응비고
rb_insert_color()mtree_store_range()범위 기반 삽입
rb_erase()mtree_erase()인덱스 기반 삭제
rb_search() (커스텀)mtree_load() / mt_find()커스텀 비교 함수 불필요
rb_first() / rb_next()mt_find() / mas_next()단일 API로 통합
rbtree_postorder_for_each_entry_safe()mt_for_each()안전한 순회
augmented rb_tree (gap)자동 내장 (arange_64 노드)별도 관리 불필요
vm_area_struct.vm_rbMaple Tree 슬롯에 직접 저장rb_node 필드 제거
vm_area_struct.vm_next/vm_prevmas_next() / mas_prev()연결 리스트 제거

마이그레이션 코드 예시

/* Linux 6.0 이전: RB-Tree + 연결 리스트로 VMA 삽입 */
static void __vma_link_rb(struct mm_struct *mm,
                            struct vm_area_struct *vma,
                            struct rb_node **rb_link,
                            struct rb_node *rb_parent)
{
    /* RB-Tree에 삽입 */
    rb_link_node(&vma->vm_rb, rb_parent, rb_link);
    rb_insert_augmented(&vma->vm_rb, &mm->mm_rb,
                         &vma_gap_callbacks);
    /* 연결 리스트 업데이트 */
    vma->vm_next = next;
    if (next) next->vm_prev = vma;
    vma->vm_prev = prev;
}

/* Linux 6.1+: Maple Tree로 VMA 삽입 */
static void vma_mt_store(struct mm_struct *mm,
                          struct vm_area_struct *vma)
{
    /* Maple Tree에 범위로 삽입 — 단일 호출로 완료 */
    vma_store(mm, vma);
    /* 내부적으로: mtree_store_range(&mm->mm_mt,
                     vma->vm_start, vma->vm_end - 1, vma, GFP_KERNEL) */
    /* RB-Tree 삽입 + augmented 갱신 + 연결 리스트 갱신을
       단일 범위 저장으로 대체 */
}

vm_area_struct 변경

/* Linux 6.0 이전 */
struct vm_area_struct {
    unsigned long    vm_start;
    unsigned long    vm_end;
    struct vm_area_struct *vm_next, *vm_prev; /* 연결 리스트 */
    struct rb_node   vm_rb;                   /* RB-Tree 노드 */
    unsigned long    rb_subtree_gap;          /* augmented gap */
    /* ... */
};

/* Linux 6.1+ */
struct vm_area_struct {
    unsigned long    vm_start;
    unsigned long    vm_end;
    /* vm_next, vm_prev 제거 */
    /* vm_rb 제거 */
    /* rb_subtree_gap 제거 */
    /* Maple Tree 슬롯에 직접 포인터로 저장 */
    /* ... */
};
주의: Maple Tree는 NULL 값을 저장할 수 없습니다 (빈 슬롯과 구별 불가). NULL 대신 특수 마커 값(XA_ZERO_ENTRY 등)을 사용하거나 별도 비트맵으로 관리해야 합니다.

성능 특성

연산시간 복잡도RB-Tree 비교비고
검색 (단일)O(log n)O(log n)낮은 상수: 캐시 라인 효율
범위 검색O(log n)O(log n + k)k=결과 수, Maple Tree가 유리
gap 검색O(log n)O(n) 또는 O(log n)**augmented 별도 유지 필요
삽입O(log n)O(log n)분할 시 메모리 할당 필요
삭제O(log n)O(log n)병합 발생 가능
순회O(n)O(n)RCU lock-free 읽기 지원
fork() VMA 복사O(n)O(n)잠금 경합 40% 감소

벤치마크 비교 (Linux 6.1)

벤치마크RB-Tree (6.0)Maple Tree (6.1)변화율
mmap/munmap 100K VMA1.00x (기준)1.08x+8% 처리량
fork() 1000 VMA1.00x1.12x+12% 처리량
mmap_sem 경합 (8코어)1.00x0.60x-40% 경합
find_vma() 레이턴시1.00x0.92x-8% 레이턴시
Java 스레드 생성 (10K VMA)1.00x1.15x+15% 처리량
메모리 오버헤드 (VMA당)~24B (rb_node + list)~16B (슬롯 당)-33%
실측 성능 해석: Maple Tree는 VMA 수가 적을 때(수십 개) RB-Tree와 차이가 미미하지만, VMA 수가 수백~수천 개로 늘어나면 캐시 효율성 차이가 극대화됩니다. Java/Go 같은 GC 기반 언어의 런타임은 VMA를 많이 생성하므로 효과가 두드러집니다. 특히 멀티코어에서 mmap_sem 경합 감소 효과가 가장 중요합니다.

트리 높이 분석

VMA 수RB-Tree 높이Maple Tree 높이 (range_64)Maple Tree 높이 (arange_64)
16512
100712
256922
1,0001123
10,0001434
100,0001745
높이의 의미: 트리 높이 = 루트에서 잎까지의 메모리 접근 횟수입니다. RB-Tree 높이 17은 17번의 캐시 라인 접근(각각 다른 메모리 위치)을 의미하지만, Maple Tree 높이 4는 4개 노드만 접근하면 됩니다. 각 노드 접근은 1~4개 캐시 라인(64~256B)을 순차적으로 읽으므로 프리페치에 유리합니다.

실전 활용 예제

드라이버에서의 범위 맵 구현

/* I/O 주소 범위 → 디바이스 맵핑 예제 */
struct io_region {
    phys_addr_t  base;
    size_t       size;
    struct device *dev;
};

struct maple_tree io_map;

static int register_io_region(phys_addr_t base, size_t size,
                                 struct device *dev)
{
    struct io_region *r = kmalloc(sizeof(*r), GFP_KERNEL);
    if (!r) return -ENOMEM;

    r->base = base;
    r->size = size;
    r->dev  = dev;

    /* [base, base+size-1] 범위에 등록 */
    return mtree_store_range(&io_map, base, base + size - 1, r, GFP_KERNEL);
}

static struct io_region *find_io_region(phys_addr_t addr)
{
    return mtree_load(&io_map, addr); /* addr이 속하는 범위 자동 검색 */
}

ID 범위 할당 예제

/* MT_FLAGS_ALLOC_RANGE: 빈 범위를 자동으로 찾아 할당 */
struct maple_tree id_map;
mt_init_flags(&id_map, MT_FLAGS_ALLOC_RANGE);

unsigned long id;
/* [0, 1023] 범위에서 빈 ID 1개 할당 */
int ret = mtree_alloc_range(&id_map, &id, my_data, 0, 1023, GFP_KERNEL);
if (!ret)
    pr_info("allocated id=%lu\n", id);

/* 해제 */
mtree_erase(&id_map, id);

procfs /proc/pid/maps 구현

/* fs/proc/task_mmu.c: /proc/pid/maps 출력 (Linux 6.1+) */
static void *m_start(struct seq_file *m, loff_t *ppos)
{
    struct proc_maps_private *priv = m->private;
    struct mm_struct *mm = priv->mm;
    struct vm_area_struct *vma;
    unsigned long last_addr = *ppos;

    /* Maple Tree로 /proc/maps 페이지 순회 */
    mmap_read_lock(mm);
    vma = find_vma(mm, last_addr);
    return vma;
}

static void *m_next(struct seq_file *m, void *v, loff_t *ppos)
{
    struct vm_area_struct *vma = v;
    /* 다음 VMA: Maple Tree 순회 (연결 리스트 제거됨) */
    return find_vma(vma->vm_mm, vma->vm_end);
}

잠금 모델 상세

외부 잠금 (VMA 트리)

/* VMA 트리는 외부 잠금(mmap_lock)을 사용 */

/* 읽기 경로: mmap_read_lock + RCU */
mmap_read_lock(mm);
vma = find_vma(mm, addr);      /* 내부: mas_find() */
mmap_read_unlock(mm);

/* 쓰기 경로: mmap_write_lock */
mmap_write_lock(mm);
vma_store(mm, vma);             /* 내부: mas_store_gfp() */
mmap_write_unlock(mm);

/* Speculative 읽기 (Linux 6.4+, per-VMA lock) */
rcu_read_lock();
vma = find_vma(mm, addr);
if (vma && vma_start_read(vma)) {
    /* per-VMA lock 획득 성공 → mmap_lock 없이 접근 */
    handle_vma(vma);
    vma_end_read(vma);
}
rcu_read_unlock();
per-VMA lock (Linux 6.4): Maple Tree 도입 이후 추가된 최적화입니다. 페이지 폴트 처리 시 mmap_lock 전체를 잡지 않고, 해당 VMA만 잠그는 방식입니다. Maple Tree의 RCU 읽기 경로와 결합하여 페이지 폴트 성능을 추가로 향상시킵니다.

커널 설정 및 디버깅

/* Kconfig 의존성 */
CONFIG_MAPLE_TREE=y        /* Linux 6.1+: 자동 활성화, 별도 설정 불필요 */
CONFIG_DEBUG_MAPLE_TREE=y  /* 디버그 빌드에서 트리 일관성 검증 활성화 */
# Maple Tree 디버깅 통계 (debugfs)
$ cat /sys/kernel/debug/maple_tree/stats

# Maple Tree 사용 지점 확인
$ grep -rn "mt_init\|mtree_store\|mas_find\|mm_mt" mm/ fs/proc/ --include="*.c"

# CONFIG_DEBUG_MAPLE_TREE 활성화 시 검증 함수
# mt_validate() — 트리 전체 일관성 검사
# mas_wr_state_check() — 쓰기 상태 검증
# 커널 빌드 시: make menuconfig → Kernel hacking → Debug maple tree

# ftrace로 Maple Tree 연산 추적
$ echo 'p:maple_probe mas_walk' > /sys/kernel/debug/tracing/kprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/kprobes/maple_probe/enable
$ cat /sys/kernel/debug/tracing/trace_pipe

검증 체크리스트

점검 항목핵심 질문권장 점검
범위 삽입/삭제start/end 경계가 일관적인가?경계값 테스트(인접/중첩/빈범위) 수행
ma_state 사용커서 재사용이 안전한가?작업 단위마다 상태 초기화 확인
메모리 할당원자 컨텍스트에서 분할 가능성이 있는가?mas_preallocate 사용 검토
NULL 저장빈 슬롯과 구분되는가?마커 엔트리 정책 문서화
RCU 올바름읽기 경로에서 RCU 보호 하에 있는가?rcu_read_lock/unlock 쌍 확인
mas_destroy사전 할당 노드가 누수되지 않는가?모든 경로에서 mas_destroy 호출

소스 코드 구조

파일내용줄 수 (약)
lib/maple_tree.c핵심 구현 (삽입, 삭제, 검색, 분할, 병합)7,000+
include/linux/maple_tree.h구조체 정의, 매크로, 인라인 함수700+
lib/test_maple_tree.c커널 셀프 테스트 (CONFIG_TEST_MAPLE_TREE)3,000+
tools/testing/radix-tree/maple.c사용자 공간 테스트 (make check)2,000+
mm/mmap.cVMA 관리 (Maple Tree 사용자)변경됨
mm/memory.c페이지 폴트 처리 (find_vma)변경됨
fs/proc/task_mmu.c/proc/pid/maps (Maple Tree 순회)변경됨
# 소스 코드 탐색
$ wc -l lib/maple_tree.c include/linux/maple_tree.h
  7142 lib/maple_tree.c
   723 include/linux/maple_tree.h

# Maple Tree 셀프 테스트 실행
$ make menuconfig  # CONFIG_TEST_MAPLE_TREE=m
$ modprobe test_maple_tree
$ dmesg | grep "maple_tree"
maple_tree: all tests passed

# 사용자 공간 테스트
$ cd tools/testing/radix-tree
$ make maple
$ ./maple  # 모든 테스트 실행

B-tree 이론적 배경

Maple Tree의 동작 원리를 이해하려면 B-tree의 기본 개념을 알아야 합니다. B-tree는 1970년 Rudolf Bayer와 Edward McCreight가 발명한 자기 균형 탐색 트리로, 디스크 I/O 최소화를 위해 설계되었지만 현대 CPU의 캐시 계층에도 동일한 원리가 적용됩니다.

B-tree 핵심 속성

속성설명Maple Tree에서
분기 인자 (Order)각 노드가 가질 수 있는 최대 자식 수range_64: 16, arange_64: 10
최소 차수루트 외 노드의 최소 자식 수 = ⌈order/2⌉range_64: 8, arange_64: 5
높이 균형모든 잎 노드가 같은 깊이동일 — 항상 높이 균형
키 정렬노드 내 키가 정렬됨pivot[] 배열이 오름차순 정렬
범위 키일반 B-tree는 점 키확장: [start, end] 범위 기반
B-tree vs B+tree vs Maple Tree:
  • B-tree: 내부 노드와 잎 노드 모두 값을 저장. 데이터베이스 인덱스에 사용.
  • B+tree: 값을 잎 노드에만 저장, 잎 노드를 연결 리스트로 연결. 파일시스템(ext4, Btrfs)에 사용.
  • Maple Tree: B-tree 변형이지만 범위(range) 키를 사용하고, RCU lock-free 읽기를 지원하며, 잎 노드 연결 없이 pivot 기반 순회를 제공. VMA 관리에 특화.

높이와 검색 비용 분석

B-tree의 높이 h는 항목 수 n과 분기 인자 b에 대해 h = O(logb n)입니다. 분기 인자가 클수록 높이가 낮아집니다.

/* 높이 공식 (최대 높이) */
h ≤ log_⌈b/2⌉(n) + 1

/* Maple Tree maple_range_64 (b=16) */
n = 100,000 VMA일 때:
h ≤ log_8(100000) + 1 ≈ 5.5 + 1 = 6 (최대)
실제: 4 (노드 활용률이 높음)

/* RB-Tree (b=2) */
n = 100,000 VMA일 때:
h ≤ 2 × log_2(100001) ≈ 2 × 17 = 34 (최대)
실제: 약 17

/* 캐시 미스 비교 (L1 miss 기준) */
RB-Tree: 17 × ~4ns = ~68ns  (각 노드가 다른 캐시 라인)
Maple:    4 × ~8ns = ~32ns  (각 노드 256B = 4 캐시 라인, 순차 접근)
→ Maple Tree가 약 2배 빠름 (캐시 프리페치 효과 포함)
VMA 수에 따른 트리 높이 비교 VMA 수 (n) 트리 높이 (h) 0 2 4 6 8 10 12 14 16 18 16 256 1K 10K 100K RB-Tree Maple Tree

메모리 레이아웃과 캐시 효율

Maple Tree의 성능 우위는 캐시 친화적 메모리 레이아웃에서 비롯됩니다. 현대 CPU에서 메모리 접근 패턴이 성능을 좌우합니다.

캐시 라인 분석

/* maple_range_64 메모리 레이아웃 (256 바이트 = 캐시 라인 4개) */

오프셋     필드                   크기      캐시 라인
──────────────────────────────────────────────────────
0x000      parent                 8B   ┐
0x008      pivot[0..6]           56B   ┤ 캐시 라인 #0 (64B)
──────────────────────────────────────────────────────
0x040      pivot[7..14]          64B   │ 캐시 라인 #1 (64B)
──────────────────────────────────────────────────────
0x080      slot[0..7]            64B   │ 캐시 라인 #2 (64B)
──────────────────────────────────────────────────────
0x0C0      slot[8..15]           64B   │ 캐시 라인 #3 (64B)
──────────────────────────────────────────────────────

/* Walk 시 접근 패턴 */
1. pivot[0..6] 읽기  → 캐시 라인 #0 로드 (순차 접근 → 프리페치 활성)
2. pivot[7..14] 읽기 → 캐시 라인 #1 로드 (이미 프리페치됨)
3. slot[offset] 읽기  → 캐시 라인 #2 또는 #3

/* RB-Tree 접근 패턴 (비교) */
1. 노드 A의 key 읽기  → 캐시 라인 X 로드 (랜덤 주소)
2. 노드 A의 left/right 읽기 → 같은 캐시 라인 (포인터 추적)
3. 노드 B의 key 읽기  → 캐시 라인 Y 로드 (전혀 다른 주소)
4. 노드 C의 key 읽기  → 캐시 라인 Z 로드 (또 다른 주소)
→ 매 단계마다 캐시 미스 (프리페치 불가능)
RB-Tree: 포인터 체이싱 노드A miss 노드B miss 노드C miss 노드D 4단계 × ~4ns 캐시 미스 = ~16ns 메모리 내 산재 — 프리페치 불가 Maple Tree: 순차 스캔 p0 p1 p2 HIT 1 노드 × 2 캐시 라인 (순차) = ~8ns 256B 연속 메모리 — HW 프리페치 활성 캐시 효율 비교 요약 RB-Tree: 노드당 ~48B, 매번 랜덤 메모리 접근 (L1 miss 확실) Maple: 노드당 256B, 순차 접근 (L1 miss 1회 → 프리페치로 나머지 hit) → 같은 높이에서도 Maple Tree가 2~3배 빠름

Slab 할당과 노드 크기

/* lib/maple_tree.c — 노드 할당 */
static struct kmem_cache *maple_node_cache;

void maple_tree_init(void)
{
    maple_node_cache = kmem_cache_create(
        "maple_tree_node",
        sizeof(struct maple_node),  /* 256 바이트 */
        sizeof(struct maple_node),  /* 256B 정렬 */
        SLAB_PANIC | SLAB_RECLAIM_ACCOUNT,
        NULL);
}

/* 노드 할당 — SLAB에서 256B 정렬된 객체 획득 */
static inline struct maple_node *mt_alloc_one(gfp_t gfp)
{
    return kmem_cache_alloc(maple_node_cache, gfp);
}

/* 노드 해제 — RCU 콜백으로 지연 해제 */
static void mt_free_rcu(struct rcu_head *head)
{
    struct maple_node *node =
        container_of(head, struct maple_node, rcu);
    kmem_cache_free(maple_node_cache, node);
}
256B 정렬의 의미: 노드가 256B 경계에 정렬되면 포인터의 하위 8비트가 항상 0이 됩니다. Maple Tree는 이 여유 비트를 노드 타입 인코딩에 활용합니다 (maple_enode 하위 비트). 이는 XArray가 사용하는 동일한 태깅 기법입니다.

노드 포인터 인코딩

Maple Tree는 포인터 하위 비트에 메타데이터를 인코딩하여 추가 메모리 없이 노드 타입을 판별합니다. 이 기법은 XArray에서 그대로 가져온 것입니다.

/* include/linux/maple_tree.h — 포인터 인코딩 */

/* 노드 타입 열거형 */
enum maple_type {
    maple_dense,       /* 0b00 — 밀집 잎 노드 */
    maple_leaf_64,     /* 0b01 — range_64 잎 노드 */
    maple_range_64,    /* 0b10 — range_64 내부 노드 */
    maple_arange_64,   /* 0b11 — arange_64 내부/잎 노드 */
};

/* 포인터 → 인코딩된 노드 (encoded node = enode) */
static inline struct maple_enode *
mt_mk_node(const struct maple_node *node, enum maple_type type)
{
    return (struct maple_enode *)
           ((unsigned long)node | (unsigned long)type);
}

/* 인코딩된 노드 → 실제 노드 포인터 */
static inline struct maple_node *
mte_to_node(const struct maple_enode *enode)
{
    return (struct maple_node *)
           ((unsigned long)enode & ~0x7F);
}

/* 인코딩된 노드 → 타입 추출 */
static inline enum maple_type
mte_node_type(const struct maple_enode *enode)
{
    return (enum maple_type)
           ((unsigned long)enode & 0x7F);
}

/* 잎 노드인지 확인 */
static inline bool ma_is_leaf(enum maple_type type)
{
    return type < maple_range_64;
}
인코딩 비트타입잎/내부용도
0b00maple_dense소수 연속 인덱스 저장
0b01maple_leaf_64range_64 형식의 잎 노드
0b10maple_range_64내부일반 범위 트리 내부 노드
0b11maple_arange_64둘 다VMA 트리 (gap 검색)
특수 포인터 값: ma_state.node는 실제 노드 외에 다음 특수 값을 가질 수 있습니다:
  • MAS_START — 초기 상태 (아직 walk하지 않음)
  • MAS_ROOT — 루트 포인터 자체 (단일 항목 최적화)
  • MAS_NONE — 범위 밖 (검색 실패)
  • MAS_PAUSE — 순회 일시 중지 (잠금 해제 후 재개용)
  • XA_ERROR(errno) — 에러 상태 인코딩

삽입 알고리즘 상세

Maple Tree의 삽입은 B-tree 삽입의 변형으로, big_node 임시 버퍼를 사용하여 분할 결정을 지연합니다.

mas_store_gfp() 삽입 흐름 1. mas_walk(): 대상 잎 노드 찾기 index/last로 해당 위치 검색 2. 현재 노드에 여유 공간 확인 여유 있음 가득 참 3a. 직접 삽입 (Fast Path) pivot 조정 + slot에 값 저장 완료! 3b. big_node 생성 (Slow Path) 기존 내용 + 새 항목 병합 4. 분할 지점 결정 중간점 기준으로 좌/우 분배 5. 새 노드 2개 할당 + 복사 좌측 + 우측 노드 생성 6. 부모 노드 갱신 (연쇄 가능)
/* lib/maple_tree.c — 삽입 핵심 로직 (간략화) */
static inline int mas_wr_modify(struct ma_wr_state *wr_mas)
{
    struct ma_state *mas = wr_mas->mas;
    unsigned char node_slots = mt_slot_count(mas->node);
    unsigned char new_end;

    /* 덮어쓰기인 경우 (기존 범위에 새 값 저장) */
    if (wr_mas->entry && wr_mas->content) {
        /* 단순 슬롯 교체 — 가장 빠른 경로 */
        rcu_assign_pointer(wr_mas->slots[mas->offset], wr_mas->entry);
        return 0;
    }

    /* 새 항목이 기존 범위를 분할하는 경우 */
    new_end = mas_wr_new_end(wr_mas);

    if (new_end < node_slots) {
        /* Fast path: 여유 공간 있음 → 슬롯 시프트 + 삽입 */
        mas_wr_slot_store(wr_mas);
    } else {
        /* Slow path: 노드 가득 참 → big_node → split */
        mas_wr_node_store(wr_mas, new_end);
        /* 내부적으로 mas_split() 호출 가능 */
    }

    return 0;
}

/* big_node: 분할 전 임시 확대 노드 */
struct maple_big_node {
    struct maple_pnode *parent;
    unsigned long pivot[MAPLE_BIG_NODE_SLOTS - 1]; /* 최대 32개 */
    union {
        struct maple_enode *slot[MAPLE_BIG_NODE_SLOTS];
        struct {
            struct maple_enode *slot[MAPLE_BIG_NODE_SLOTS];
            unsigned long gap[MAPLE_BIG_NODE_SLOTS];
        };
    };
    unsigned char b_end;
    enum maple_type type;
};
big_node 전략: 삽입 시 노드가 가득 차면, 기존 내용과 새 항목을 모두 스택 상의 maple_big_node에 모읍니다. 이 큰 버퍼에서 최적 분할 지점을 결정한 후, 좌/우 두 개의 실제 노드로 분배합니다. 이 방법은 단순한 중간점 분할보다 공간 활용률이 높습니다.

삭제 알고리즘 상세

삭제는 삽입의 역과정이지만, 추가적인 병합(coalesce) 로직이 필요합니다.

/* lib/maple_tree.c — 삭제 핵심 */
void *mas_erase(struct ma_state *mas)
{
    void *entry;
    MA_WR_STATE(wr_mas, mas, NULL);

    /* 1. 대상 항목 찾기 */
    entry = mas_walk(mas);
    if (!entry)
        return NULL;

    /* 2. 해당 슬롯을 NULL로 설정 (= gap 생성) */
    mas_wr_store_setup(&wr_mas);
    mas_wr_modify(&wr_mas);
    /* entry 값 = NULL → 해당 범위가 빈 공간이 됨 */

    /* 3. 노드가 희소해지면 병합 시도 */
    if (mas_is_underflow(mas))
        mas_coalesce(mas);

    return entry;
}

/* 병합 조건: 인접 형제 노드와 합쳐서 하나의 노드로 */
static bool mas_is_underflow(struct ma_state *mas)
{
    unsigned char end = mas_data_end(mas);
    unsigned char min = mt_min_slots(mas->node);
    return end < min;
    /* range_64: min = 8 (16/2) */
    /* arange_64: min = 5 (10/2) */
}
삭제 시나리오동작복잡도
단순 삭제 (여유 있음)slot을 NULL로 설정, pivot 조정O(1) 노드 수정
범위 중간 삭제범위를 둘로 분할 후 중간 NULL 삽입O(1) + 분할 가능
인접 NULL 병합연속된 NULL 슬롯을 하나로 합침O(1) 노드 내
언더플로 → 병합형제 노드와 합쳐서 하나의 노드로O(log n) 부모 전파
트리 높이 감소루트 자식이 1개면 자식을 새 루트로O(1)

Gap 검색 알고리즘 상세

mas_empty_area()는 mmap에서 빈 가상 주소 공간을 찾는 핵심 연산입니다. maple_arange_64 노드의 gap[] 배열을 활용하여 O(log n)에 수행됩니다.

/* lib/maple_tree.c — gap 검색 (Bottom-up) */
int mas_empty_area(struct ma_state *mas,
                    unsigned long min, unsigned long max,
                    unsigned long size)
{
    unsigned char offset;
    unsigned long *pivots, *gaps;
    struct maple_node *node;

    /* 1. 루트에서 시작하여 충분한 gap이 있는 자식을 선택 */
    mas_start(mas);

    while (!mas_is_leaf(mas)) {
        node = mte_to_node(mas->node);
        gaps = ma_gaps(node, mte_node_type(mas->node));
        pivots = ma_pivots(node, mte_node_type(mas->node));

        /* 2. gap[i] >= size 인 첫 번째 슬롯 찾기 */
        for (offset = 0; offset <= ma_data_end(node); offset++) {
            if (gaps[offset] >= size &&
                pivots[offset] >= min)
                break;
        }

        /* 3. 해당 자식으로 내려감 */
        mas->offset = offset;
        mas->node = mas_get_slot(mas, offset);
        mas->depth++;
    }

    /* 4. 잎 노드에서 실제 빈 범위 확인 */
    /* NULL 슬롯 = 빈 공간, pivot 간 차이 = 공간 크기 */
    return mas_leaf_empty_area(mas, min, max, size);
}
Topdown vs Bottomup gap 검색:
  • mas_empty_area(): 낮은 주소부터 검색 (일반 mmap)
  • mas_empty_area_rev(): 높은 주소부터 검색 (스택 확장, ASLR)
리눅스 기본 mmap은 아키텍처에 따라 topdown(x86-64)을 사용합니다. 스택은 높은 주소에서 낮은 주소로 자라므로 mas_empty_area_rev()가 호출됩니다.

fork() 시 Maple Tree 복사

fork() 시스템 콜은 부모 프로세스의 전체 VMA 트리를 자식에게 복사해야 합니다. Maple Tree는 이 과정에서 RB-Tree보다 효율적인 벌크 복사를 지원합니다.

/* kernel/fork.c → mm/mmap.c */
static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
    struct vm_area_struct *vma, *new_vma;
    MA_STATE(old_mas, &oldmm->mm_mt, 0, 0);
    MA_STATE(new_mas, &mm->mm_mt, 0, 0);
    int retval;

    /* 1. 부모 VMA 순회 + 자식 트리에 삽입 */
    mas_for_each(&old_mas, vma, ULONG_MAX) {
        /* VMA 복사 */
        new_vma = vm_area_dup(vma);
        if (!new_vma)
            goto fail;

        /* 페이지 테이블 COW 설정 */
        retval = copy_page_range(mm, oldmm, vma, new_vma);

        /* 자식 Maple Tree에 벌크 삽입 */
        new_mas.index = new_vma->vm_start;
        new_mas.last  = new_vma->vm_end - 1;
        mas_store(&new_mas, new_vma);
    }

    return 0;
}
fork() 단계RB-Tree (6.0)Maple Tree (6.1+)
VMA 순회vm_next 연결 리스트mas_for_each (pivot 순회)
VMA 삽입rb_insert_color + augmented 갱신mas_store (벌크 삽입 최적화)
잠금mmap_write_lock (전체)mmap_write_lock + 세밀한 잠금
gap 정보augmented tree 전체 재계산삽입 시 자동 갱신 (arange_64)
연결 리스트vm_next/vm_prev 갱신 필요필요 없음 (제거됨)
경합 (8코어)높음 (mmap_sem 전체 잠금)40% 감소
벌크 삽입 최적화: fork() 중 자식 트리는 비어있는 상태에서 시작하므로, 순서대로 VMA를 삽입하면 Maple Tree가 내부적으로 벌크 빌드 경로를 활용합니다. 이미 정렬된 순서로 삽입되므로 분할 빈도가 낮고, 노드 활용률이 높습니다.

메모리 오버헤드 분석

Maple Tree는 노드 크기가 크지만(256B), 노드당 저장하는 항목 수가 많아 전체 메모리 사용량은 RB-Tree와 비슷하거나 더 작습니다.

항목RB-TreeMaple Tree (range_64)Maple Tree (arange_64)
노드 크기~24B (rb_node 내장)256B256B
노드당 항목1최대 16최대 10
항목당 바이트~24B~16B (256/16)~26B (256/10)
추가 오버헤드vm_rb + vm_next/vm_prev = ~24B없음없음
총 항목당 비용~48B~16B~26B
/* 1000개 VMA를 저장할 때 메모리 사용량 비교 */

RB-Tree:
  vm_area_struct: 각각 rb_node(24B) + list_head(16B) = 40B 추가
  총: 1000 × 40B = 40,000B = ~39KB

Maple Tree (arange_64):
  노드 수: 1000 / 10 = 100 잎 노드 + ~11 내부 노드 ≈ 111 노드
  총: 111 × 256B = 28,416B = ~28KB
  절감: ~28% 메모리 절약

Maple Tree (range_64):
  노드 수: 1000 / 16 = 63 잎 노드 + ~5 내부 노드 ≈ 68 노드
  총: 68 × 256B = 17,408B = ~17KB
  절감: ~57% 메모리 절약
vm_area_struct 축소: Maple Tree 도입으로 vm_area_struct에서 rb_node(24B), vm_next(8B), vm_prev(8B), rb_subtree_gap(8B) = 총 48B가 제거되었습니다. 1000개 VMA 기준으로 VMA 구조체 자체에서도 48KB가 절약됩니다.

고급 사용 패턴

노드 사전 할당 (원자적 컨텍스트)

/* 인터럽트 컨텍스트 등에서 GFP_KERNEL 불가 시 */
MA_STATE(mas, &mt, start, end);
int ret;

/* 1. 필요한 노드를 미리 할당 (슬립 가능 컨텍스트에서) */
ret = mas_preallocate(&mas, entry, GFP_KERNEL);
if (ret)
    return ret;

/* 2. 원자적 컨텍스트에서 삽입 (메모리 할당 불필요) */
spin_lock_irqsave(&my_lock, flags);
mas_lock(&mas);
mas_store_prealloc(&mas, entry);  /* GFP 불필요 */
mas_unlock(&mas);
spin_unlock_irqrestore(&my_lock, flags);

/* 3. 미사용 사전 할당 노드 반환 */
mas_destroy(&mas);

벌크 연산

/* 대량 항목을 효율적으로 삽입 */
MA_STATE(mas, &mt, 0, 0);

mas_lock(&mas);
for (int i = 0; i < count; i++) {
    mas.index = ranges[i].start;
    mas.last  = ranges[i].end;

    /* mas가 이전 위치를 기억하므로 연속 삽입이 빠름 */
    mas_store_gfp(&mas, entries[i], GFP_KERNEL);
}
mas_unlock(&mas);
mas_destroy(&mas);

/* 대량 삭제: 범위 지정 삭제 */
mtree_store_range(&mt, start, end, NULL, GFP_KERNEL);
/* [start, end] 범위의 모든 항목이 NULL로 덮어씌워짐 */

순회 중 일시 중지/재개

/* RCU 보호 순회에서 오래 걸리는 작업이 있을 때 */
MA_STATE(mas, &mt, 0, 0);
void *entry;

rcu_read_lock();
mas_for_each(&mas, entry, ULONG_MAX) {
    if (need_resched()) {
        /* 순회 일시 중지 */
        mas_pause(&mas);
        rcu_read_unlock();
        cond_resched();
        rcu_read_lock();
        /* 재개: mas_for_each가 자동으로 이전 위치에서 계속 */
        continue;
    }
    process_entry(entry);
}
mas_pause() 주의점: 일시 중지 후 재개하면 트리가 변경되었을 수 있습니다. mas_for_each는 다음 항목을 검색하므로 삭제된 항목은 건너뛰고, 새로 삽입된 항목은 인덱스 범위에 따라 포함될 수 있습니다. 이는 RCU의 일반적인 동작과 일치합니다.

기타 커널 사용처

VMA 관리 외에도 Maple Tree는 커널 내 여러 서브시스템에서 범위 기반 맵으로 활용됩니다.

사용처용도노드 타입특이점
mm/mmap.cVMA 관리 (mm_mt)arange_64gap 검색, 외부 잠금
fs/proc/task_mmu.c/proc/pid/maps 출력읽기 전용 순회RCU lock-free
mm/madvise.cmadvise 범위 처리읽기+수정범위 순회 + VMA 분할
mm/mlock.cmlock 범위 처리수정VMA 플래그 변경
mm/mprotect.cmprotect 범위 처리수정VMA 분할/병합
mm/oom_kill.cOOM 킬러 VMA 순회읽기 전용메모리 사용량 계산
kernel/fork.cfork() VMA 복사벌크 삽입정렬된 순서 삽입
kernel/events/uprobes.cuprobe 맵핑읽기 전용 순회VMA에서 offset 계산

per-VMA Lock 연동 (Linux 6.4+)

Maple Tree 도입 이후, Linux 6.4에서는 per-VMA lock이라는 추가 최적화가 도입되었습니다. 이는 Maple Tree의 RCU 읽기 경로를 활용하여, 페이지 폴트 처리 시 mmap_lock 전체를 잡지 않고 개별 VMA만 잠그는 방식입니다.

per-VMA Lock: 페이지 폴트 처리 경로 기존 (Linux 6.0): mmap_lock 전체 잠금 1. mmap_read_lock(mm) — 전역 잠금 2. find_vma() — VMA 검색 3. handle_mm_fault() — 페이지 폴트 처리 4. mmap_read_unlock(mm) — 잠금 해제 병목: 모든 폴트가 mmap_lock 경합 새로운 (Linux 6.4+): per-VMA lock 1. rcu_read_lock() — 경량 RCU 2. find_vma() — Maple Tree RCU 읽기 3. vma_start_read(vma) — VMA만 잠금 4. handle_mm_fault() + vma_end_read() 이점: 서로 다른 VMA 폴트는 병렬 처리
/* mm/memory.c — per-VMA lock 기반 페이지 폴트 (Linux 6.4+) */
static inline vm_fault_t
do_user_addr_fault(struct pt_regs *regs, unsigned long error_code,
                    unsigned long address)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma;

    /* Fast path: per-VMA lock 시도 */
    rcu_read_lock();
    vma = find_vma(mm, address);  /* Maple Tree RCU 읽기 */
    if (vma && vma_start_read(vma)) {
        /* VMA 전용 잠금 획득 성공 → mmap_lock 불필요 */
        rcu_read_unlock();
        fault = handle_mm_fault(vma, address, flags, regs);
        vma_end_read(vma);
        return fault;
    }
    rcu_read_unlock();

    /* Fallback: per-VMA lock 실패 → mmap_lock 사용 */
    mmap_read_lock(mm);
    vma = find_vma(mm, address);
    fault = handle_mm_fault(vma, address, flags, regs);
    mmap_read_unlock(mm);
    return fault;
}
성능 효과: per-VMA lock은 Maple Tree의 RCU lock-free 읽기 위에 구축된 최적화입니다. 멀티스레드 애플리케이션에서 서로 다른 VMA에 대한 페이지 폴트가 완전히 병렬로 처리되어, 75% 이상의 페이지 폴트에서 mmap_lock 경합이 제거됩니다 (실측: Intel Xeon 96코어 서버 기준).

향후 발전

방향상태설명
per-VMA lock 확장6.4+ 진행 중더 많은 mmap 연산에 VMA 단위 잠금 적용
VMA 구조체 축소6.5 완료vm_next/vm_prev 완전 제거, 구조체 크기 감소
userfaultfd 통합6.3 완료userfaultfd가 Maple Tree API 사용
ASLR 개선논의 중gap 검색 알고리즘 활용한 더 균일한 주소 분배
다른 서브시스템 확장논의 중I/O 범위 맵, 리소스 관리 등 범위 기반 맵 대체
Lazy free 최적화실험적munmap 시 Maple Tree 노드 지연 해제로 성능 개선
Linux 6.x 타임라인:
  • 6.1: Maple Tree 도입, VMA RB-Tree 대체
  • 6.2: Maple Tree 버그 수정, 안정화
  • 6.3: userfaultfd Maple Tree 통합
  • 6.4: per-VMA lock 도입 (Maple Tree RCU 활용)
  • 6.5: vm_next/vm_prev 완전 제거, VMA 구조체 정리
  • 6.7+: per-VMA lock 확장, 추가 최적화

트러블슈팅

일반적인 문제

증상원인해결
BUG: maple_tree 커널 패닉트리 일관성 깨짐CONFIG_DEBUG_MAPLE_TREE=y로 재빌드, 삽입/삭제 경로 검증
-ENOMEM 삽입 실패노드 분할 시 메모리 부족mas_nomem() 재시도 패턴 사용, mas_preallocate() 검토
메모리 누수 (slab 증가)mas_destroy() 미호출모든 ma_state 경로에서 mas_destroy() 보장
-EEXIST 삽입 거부중복 범위 (mtree_insert 사용 시)mtree_store()로 변경 (기존 값 덮어쓰기)
RCU 경고 (lockdep)읽기 경로에서 RCU 보호 누락rcu_read_lock() 추가 또는 mtree_load() 사용 (자동 RCU)
순회 중 항목 누락mas_pause() 없이 잠금 해제잠금 해제 전 mas_pause(), 재잠금 후 자동 재개
성능 저하 (기대 이하)range_64 대신 arange_64 불필요 사용gap 검색 불필요 시 MT_FLAGS_ALLOC_RANGE 제거

디버깅 도구

/* CONFIG_DEBUG_MAPLE_TREE=y 활성화 시 사용 가능한 함수 */

/* 트리 전체 일관성 검증 (느림, 디버그 전용) */
mt_validate(&mt);

/* 트리 구조 덤프 (printk 출력) */
mt_dump(&mt, mt_dump_hex);   /* 16진수 모드 */
mt_dump(&mt, mt_dump_dec);   /* 10진수 모드 */

/* 출력 예시: */
/*
 * maple_arange64 0xffff...0000 parent 0 height 2
 *   entry 0: 0xffff...1000 [0 - 0x1FFF]
 *   entry 1: 0xffff...2000 [0x2000 - 0x3FFF]
 *   entry 2: (nil) [0x4000 - 0x6FFF]  ← gap
 *   ...
 */
# ftrace로 Maple Tree 연산 추적
echo 'p:mt_store mtree_store_range mt=%di:x64 start=%si:x64 end=%dx:x64' \
    > /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/mt_store/enable
cat /sys/kernel/debug/tracing/trace_pipe

# bpftrace로 삽입/삭제 레이턴시 측정
bpftrace -e '
kprobe:mtree_store_range { @start[tid] = nsecs; }
kretprobe:mtree_store_range /@start[tid]/ {
    @latency = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
'

# SLAB 통계로 노드 수 확인
cat /proc/slabinfo | grep maple_tree
# maple_tree_node    12345  12800   256   16    1 : ...
프로덕션 주의: CONFIG_DEBUG_MAPLE_TREE는 모든 삽입/삭제 후 트리 전체를 검증하므로 성능이 100~1000배 저하됩니다. 디버깅 목적으로만 사용하고, 프로덕션 커널에서는 반드시 비활성화하세요.

API 레퍼런스 요약

라이프사이클 API

함수설명잠금
DEFINE_MTREE(name)정적 선언 + 초기화-
mt_init(&mt)동적 초기화 (기본 모드)-
mt_init_flags(&mt, flags)플래그 지정 초기화-
mtree_destroy(&mt)전체 해제 (모든 노드 free)내장
mt_set_external_lock(&mt, lock)외부 잠금 설정-

CRUD API

함수설명잠금반환
mtree_load(&mt, idx)인덱스로 값 조회RCU 자동값 또는 NULL
mtree_store(&mt, idx, val, gfp)인덱스에 값 저장 (덮어쓰기)내장0 또는 에러
mtree_store_range(&mt, s, e, val, gfp)범위에 값 저장내장0 또는 에러
mtree_insert(&mt, idx, val, gfp)삽입 (중복 거부)내장0 또는 -EEXIST
mtree_insert_range(&mt, s, e, val, gfp)범위 삽입 (중복 거부)내장0 또는 -EEXIST
mtree_erase(&mt, idx)인덱스 삭제내장이전 값
mt_find(&mt, &idx, max)idx 이후 첫 항목RCU 자동값, idx 갱신
mt_find_after(&mt, &idx, max)idx 다음 첫 항목RCU 자동값, idx 갱신

ma_state 고급 API

함수설명사전 조건
MA_STATE(name, mt, first, last)스택에 커서 초기화-
mas_lock(&mas)내장 스핀락 획득-
mas_unlock(&mas)내장 스핀락 해제mas_lock 후
mas_walk(&mas)index 위치로 이동-
mas_store_gfp(&mas, val, gfp)현재 범위에 값 저장mas_lock
mas_store_prealloc(&mas, val)사전 할당된 노드로 저장mas_preallocate
mas_erase(&mas)현재 항목 삭제외부 잠금
mas_find(&mas, max)다음 항목 찾기RCU
mas_find_rev(&mas, min)이전 항목 찾기RCU
mas_next(&mas, max)다음 항목 이동RCU
mas_prev(&mas, min)이전 항목 이동RCU
mas_pause(&mas)순회 일시 중지순회 중
mas_empty_area(&mas, min, max, sz)빈 공간 찾기 (정방향)ALLOC_RANGE
mas_empty_area_rev(&mas, min, max, sz)빈 공간 찾기 (역방향)ALLOC_RANGE
mas_preallocate(&mas, val, gfp)노드 사전 할당-
mas_nomem(&mas, gfp)메모리 부족 시 재할당 + true실패 후
mas_destroy(&mas)사전 할당 노드 반환반드시 호출
mas_for_each(&mas, entry, max)순회 매크로RCU

RCU-safe 연산 심화

Maple Tree에서 RCU-safe 연산은 단순한 읽기/쓰기 분리를 넘어, 노드 수준의 원자적 교체와 grace period 관리를 포함합니다. mas_store_gfp()가 RCU 모드에서 동작하는 과정을 상세히 살펴봅니다.

mas_store_gfp() RCU 내부 동작

mas_store_gfp() RCU 모드 내부 흐름 1. Walk mas_walk()로 대상 잎 노드 탐색 2. 용량 확인 기존 노드에 여유 슬롯 있는가? 여유 있음 3a. 새 노드 복사 기존 노드 복사 + 새 항목 삽입 (COW) 가득 참 3b. 분할 (Split) big_node에 데이터 복사 → 좌/우 새 노드 생성 3b'. 부모 갱신 부모 노드에도 COW 새 pivot + slot 추가 4. rcu_assign_pointer() 부모 슬롯을 새 노드로 원자적 교체 (write barrier 포함) 5. call_rcu() — 구 노드 지연 해제 모든 Reader의 rcu_read_unlock() 완료 후 mt_free_rcu() 호출 동시 Reader rcu_read_lock() 구 노드 or 신 노드 둘 다 안전하게 접근 rcu_read_unlock()
/* RCU 모드 저장의 핵심: 노드 수준 Copy-on-Write */
static inline void mas_store_rcu(struct ma_state *mas, void *entry)
{
    struct maple_enode *old_node = mas->node;
    struct maple_node *new_node;

    /* 1. 새 노드 할당 */
    new_node = mt_alloc_one(GFP_NOWAIT);
    if (!new_node)
        return;

    /* 2. 기존 노드 내용을 새 노드로 복사 */
    memcpy(new_node, mte_to_node(old_node),
           sizeof(struct maple_node));

    /* 3. 새 노드에 변경 사항 적용 */
    mte_set_slot(new_node, mas->offset, entry);

    /* 4. 부모 슬롯을 원자적으로 교체 (메모리 배리어 포함) */
    rcu_assign_pointer(
        mte_parent_slot(old_node),
        mt_mk_node(new_node, mte_node_type(old_node))
    );

    /* 5. 구 노드를 grace period 후 해제 */
    call_rcu(&mte_to_node(old_node)->rcu, mt_free_rcu);
}

Lock-free 읽기 경로 상세

/* Lock-free 읽기: rcu_dereference()가 핵심 */
static inline void *mas_walk_rcu(struct ma_state *mas)
{
    struct maple_enode *node;
    void *entry;
    unsigned char offset;

retry:
    node = rcu_dereference(mas->tree->ma_root);

    while (!mte_is_leaf(node)) {
        unsigned long *pivots = ma_pivots(mte_to_node(node));
        void __rcu **slots = ma_slots(mte_to_node(node));

        /* pivot 스캔으로 슬롯 결정 */
        offset = 0;
        while (offset < mt_pivot_count(node) &&
               pivots[offset] < mas->index)
            offset++;

        /* RCU 보호 하에 자식 포인터 읽기 */
        node = rcu_dereference(slots[offset]);

        /* 동시 수정 감지: dead 노드면 재시도 */
        if (unlikely(ma_dead_node(node)))
            goto retry;
    }

    /* 잎 노드에서 값 읽기 */
    entry = rcu_dereference(ma_slots(mte_to_node(node))[offset]);
    return entry;
}
RCU 프리미티브사용 위치역할
rcu_dereference()읽기 경로 (슬롯 포인터 읽기)메모리 배리어 + 컴파일러 배리어로 최신 포인터 보장
rcu_assign_pointer()쓰기 경로 (슬롯 포인터 교체)쓰기 배리어 후 포인터 게시 — Reader에게 일관된 데이터 노출
call_rcu()구 노드 해제grace period 후 콜백 실행 — 모든 Reader가 구 노드 참조 해제 보장
ma_dead_node()읽기 경로 (재시도 판단)COW 교체된 구 노드를 감지하여 루트부터 재시도
Dead 노드 감지: RCU 읽기 경로에서 쓰기 측이 노드를 교체하면, 읽기 측은 이미 해제 예약된 "dead" 노드를 따라갈 수 있습니다. ma_dead_node()는 노드의 parent 포인터가 특수 값으로 마킹되었는지 확인하여 이를 감지합니다. Dead 노드를 만나면 루트부터 재시도합니다. RCU grace period 내이므로 노드 메모리 자체는 아직 유효합니다.

노드 타입 심화

Maple Tree의 세 가지 노드 타입(maple_range_64, maple_arange_64, maple_dense)은 각각 다른 사용 패턴에 최적화되어 있습니다. 여기서는 pivot 인코딩 방식과 노드 타입 전환 규칙을 상세히 분석합니다.

Pivot 인코딩 심화

Pivot 인코딩: 범위 경계 표현 방식 maple_range_64 잎 노드 (node_min=0x1000, node_max=0xFFFF) pivot[0]=0x2FFF pivot[1]=0x4FFF pivot[2]=0x7FFF pivot[3]=0xBFFF pivot[4]=0 pivot[5..14]=0 slot[0]=VMA_A slot[1]=NULL slot[2]=VMA_B slot[3]=VMA_C slot[4]=VMA_D [0x1000, 0x2FFF] VMA_A [0x3000, 0x4FFF] gap (빈 공간) [0x5000, 0x7FFF] VMA_B [0x8000, 0xBFFF] VMA_C [0xC000, 0xFFFF] VMA_D Pivot 인코딩 규칙 1. slot[0]의 범위: [node_min, pivot[0]] 2. slot[i]의 범위 (i > 0): [pivot[i-1] + 1, pivot[i]] 3. 마지막 유효 slot: pivot[i] == 0이면, 이전 slot까지만 유효 (meta.end로 판단) 4. 마지막 slot의 상한: 부모에서 전달받은 node_max (노드 자체에 저장되지 않음) 5. slot[i] == NULL이면 해당 범위는 빈 공간 (gap) 주의: pivot[i] = 0은 "0이라는 값"이 아니라 "미사용"을 의미 (index 0만 예외적으로 처리)
/* maple_range_64 구조에서 pivot 해석 규칙 */

/* 슬롯 i의 범위를 계산하는 내부 함수 */
static inline void mas_slot_range(
    struct ma_state *mas,
    unsigned char offset,
    unsigned long *r_min,
    unsigned long *r_max)
{
    unsigned long *pivots = ma_pivots(mte_to_node(mas->node));

    /* 하한: slot[0]이면 node_min, 아니면 pivot[offset-1] + 1 */
    *r_min = offset ? pivots[offset - 1] + 1 : mas->min;

    /* 상한: 마지막 유효 슬롯이면 node_max, 아니면 pivot[offset] */
    if (offset < mt_pivot_count(mas->node) && pivots[offset])
        *r_max = pivots[offset];
    else
        *r_max = mas->max;
}

/* maple_arange_64의 gap 필드 해석 */
/*
 * gap[i] = slot[i]가 가리키는 서브트리 내 최대 연속 빈 공간 크기
 *
 * 잎 노드:   gap[i] = slot[i]가 NULL인 범위의 크기
 * 내부 노드: gap[i] = max(자식 노드의 모든 gap[j])
 *
 * meta.gap = gap[] 배열에서 최대값을 가진 인덱스
 *            → 빠른 최대 gap 위치 조회용
 */
struct maple_metadata {
    unsigned char end;   /* 마지막 유효 슬롯 인덱스 */
    unsigned char gap;   /* 최대 gap을 가진 슬롯 인덱스 */
};

노드 타입 선택 규칙

조건선택 노드 타입이유
MT_FLAGS_ALLOC_RANGE 설정maple_arange_64gap 검색 지원을 위해 gap[] 배열 필요
플래그 없음 + 범위 항목maple_range_64최대 슬롯 수(16)로 높은 팬아웃
인덱스 0~63 연속 항목maple_densepivot 없이 직접 인덱싱으로 최소 오버헤드
트리 높이 0 + 항목 1개루트 직접 저장노드 할당 없이 ma_root에 값 포인터 직접 저장
maple_leaf_64: 별도의 "잎 노드" 구조체는 없습니다. maple_range_64maple_arange_64가 잎/내부 노드 모두로 사용되며, 노드 포인터의 인코딩 비트로 잎/내부 여부를 구분합니다. 잎 노드의 slot[]에는 자식 노드 대신 사용자 값 포인터가 저장됩니다.

벌크 연산과 사전 할당

대량의 항목을 순서대로 삽입해야 하는 경우(예: fork()에서 VMA 복사), Maple Tree는 벌크 연산 최적화를 제공합니다. mas_expected_entries()를 통해 필요한 노드를 미리 할당하면 삽입 중 메모리 할당 실패를 방지할 수 있습니다.

mas_expected_entries() 사전 할당

/* kernel/fork.c — fork() 시 VMA 벌크 복사 */
static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
    MA_STATE(old_mas, &oldmm->mm_mt, 0, 0);
    MA_STATE(mas, &mm->mm_mt, 0, 0);
    struct vm_area_struct *vma, *new_vma;
    int retval;

    /* 핵심: 예상 항목 수만큼 노드 사전 할당
     * oldmm의 VMA 수(map_count)를 기반으로 필요한 노드를 미리 할당
     * → 삽입 루프 중 메모리 할당이 불필요 */
    retval = mas_expected_entries(&mas, oldmm->map_count);
    if (retval)
        goto out;

    /* 기존 VMA를 순서대로 복사하여 새 트리에 삽입 */
    mas_for_each(&old_mas, vma, ULONG_MAX) {
        new_vma = vm_area_dup(vma);
        if (!new_vma)
            goto fail;

        /* 사전 할당된 노드를 사용하여 삽입 (메모리 할당 없음) */
        mas.index = new_vma->vm_start;
        mas.last  = new_vma->vm_end - 1;
        mas_store(&mas, new_vma);
    }

    /* 미사용 사전 할당 노드 반환 (반드시 호출) */
    mas_destroy(&mas);
    return 0;
fail:
    mas_destroy(&mas);
    return -ENOMEM;
}

사전 할당 노드 수 계산

/* lib/maple_tree.c — 필요 노드 수 계산 */
int mas_expected_entries(struct ma_state *mas, unsigned long nr_entries)
{
    int nonleaf_cap = MAPLE_ARANGE64_SLOTS;  /* 10 (arange) 또는 16 (range) */
    int leaf_cap    = MAPLE_RANGE64_SLOTS;   /* 16 */
    int nr_nodes;

    /* 잎 노드 수 = ceil(nr_entries / leaf_cap) */
    nr_nodes = DIV_ROUND_UP(nr_entries, leaf_cap);

    /* 내부 노드 수 = 잎 노드 수에 대한 기하 급수 합
     * level 1: ceil(nr_leaves / nonleaf_cap)
     * level 2: ceil(level1_nodes / nonleaf_cap)
     * ... 루트까지 */
    {
        int level_nodes = nr_nodes;
        while (level_nodes > 1) {
            level_nodes = DIV_ROUND_UP(level_nodes, nonleaf_cap);
            nr_nodes += level_nodes;
        }
    }

    /* 분할/재균형 여분 추가 (약 10~20%) */
    nr_nodes += nr_nodes / 10;

    /* mas->alloc에 미리 할당 */
    mas_node_count_gfp(mas, nr_nodes, GFP_KERNEL);
    return mas_is_err(mas) ? -ENOMEM : 0;
}
VMA 수잎 노드내부 노드총 노드총 메모리 (256B/노드)
1007182 KB
1,0006377018 KB
10,00062570695174 KB
65,5354,0964554,5511.1 MB
벌크 삽입 최적화: 정렬된 순서로 삽입하면 Maple Tree는 "append" 모드로 동작하여 분할 빈도가 최소화됩니다. fork()의 VMA 복사는 주소 순서대로 진행되므로 이 최적화의 혜택을 받습니다. 무작위 삽입 대비 약 30% 적은 노드 분할이 발생합니다.

커널 자료구조 비교

Linux 커널에는 여러 범위/키-값 자료구조가 공존합니다. 각각의 특성과 적합한 사용 사례를 비교합니다.

커널 범위/키-값 자료구조 계보 Radix Tree (deprecated) XArray 인덱스 키, 페이지 캐시 대체 RB-Tree (VMA) (~Linux 6.0) Maple Tree 범위 키, VMA (6.1+) 대체 코드 기반 IDR / IDA ID 할당 (XArray 기반) RB-Tree (범용) 스케줄러, I/O 등 Interval Tree 겹침 검색 (RB 기반) 핵심 차이 요약 RB-Tree: 노드당 1키, 범용 비교함수, 인라인 노드(구조체 임베딩), 캐시 비친화적 XArray: 인덱스 기반 trie, 페이지 캐시 특화, RCU 지원, 범위 저장 미지원 Maple Tree: 범위 키 B-tree, gap 검색 내장, RCU 지원, 캐시 친화적 (256B 노드) IDR/IDA: 정수 ID 할당 특화, XArray 위에 구축, 연속 ID 할당에 최적화
특성RB-TreeRadix/XArrayMaple TreeIDR
키 타입비교 함수정수 인덱스범위 [start,end]정수 ID
노드 크기~40B (임베딩)576B (xa_node)256BXArray 기반
팬아웃2 (이진)6410~1664
RCU 읽기불가지원지원지원
범위 저장수동 구현불가네이티브불가
Gap 검색O(n) / augmented불가O(log n) 내장O(log n)
캐시 효율낮음 (분산)높음높음 (256B 정렬)높음
잠금외부내장/외부내장/외부내장
대표 사용처스케줄러, 범용페이지 캐시VMA 관리fd, inode ID
선택 가이드:
  • 정렬된 단일 키 맵이 필요하고 노드 임베딩이 중요 → RB-Tree
  • 정수 인덱스 + RCU 읽기 + 페이지 캐시 → XArray
  • 범위 기반 맵 + gap 검색 + RCU 읽기 → Maple Tree
  • 정수 ID 자동 할당 → IDR/IDA

VMA 관리 케이스 스터디

mmap, munmap, mremap 시스템 콜이 Maple Tree를 통과하는 경로를 상세히 추적합니다.

mmap() 경로

mmap() 시스템 콜 → Maple Tree 경로 sys_mmap() 사용자 요청 진입 do_mmap() 파라미터 검증 mmap_region() mmap_write_lock 보유 3a. mas_empty_area[_rev]() arange_64 gap으로 빈 주소 검색 3b. vma_merge() 인접 VMA와 병합 시도 3c. vma_mt_store() Maple Tree에 VMA 삽입 mtree_store_range(&mm->mm_mt, vma->vm_start, vma->vm_end - 1, vma, GFP_KERNEL) 범위 [vm_start, vm_end-1] → VMA 포인터 저장 결과: Maple Tree 노드 내 pivot/slot에 새 VMA 범위가 기록됨 gap[] 배열 자동 갱신 → 다음 mmap 호출의 gap 검색에 반영

munmap() 경로

/* mm/mmap.c — munmap 시 Maple Tree 연산 (Linux 6.1+) */
static int do_munmap(struct mm_struct *mm,
                      unsigned long start, size_t len,
                      struct list_head *uf)
{
    MA_STATE(mas, &mm->mm_mt, start, start + len - 1);
    struct vm_area_struct *vma;

    /* 1. 해당 범위의 VMA 찾기 */
    vma = mas_find(&mas, start + len - 1);
    if (!vma)
        return -ENOMEM;

    /* 2. VMA가 요청 범위와 부분 겹침이면 분할 */
    if (vma->vm_start < start) {
        /* 앞쪽 분할: VMA를 start 위치에서 자르기 */
        __split_vma(mm, vma, start, 0);
        /* Maple Tree: 기존 범위 축소 + 새 VMA 삽입 */
    }
    if (vma->vm_end > start + len) {
        /* 뒤쪽 분할: VMA를 start+len 위치에서 자르기 */
        __split_vma(mm, vma, start + len, 1);
    }

    /* 3. 범위 내 모든 VMA 삭제 */
    mas_set_range(&mas, start, start + len - 1);
    mas_store_prealloc(&mas, NULL);
    /* NULL 저장 = 범위 삭제, gap[] 자동 갱신 */

    return 0;
}

mremap() 경로

/* mm/mremap.c — mremap 시 Maple Tree 연산 */
static unsigned long move_vma(struct vm_area_struct *vma,
                                unsigned long old_addr, unsigned long old_len,
                                unsigned long new_len, unsigned long new_addr)
{
    struct mm_struct *mm = vma->vm_mm;

    /* 1. 새 위치에 빈 공간 확인 (mas_empty_area) */
    /* 2. 구 위치에서 VMA 삭제 (Maple Tree store NULL) */
    do_munmap(mm, old_addr, old_len, &uf);

    /* 3. 새 위치에 VMA 삽입 (Maple Tree store) */
    vma->vm_start = new_addr;
    vma->vm_end   = new_addr + new_len;
    vma_mt_store(mm, vma);

    /* 4. 페이지 테이블 이동 (Maple Tree 무관) */
    move_page_tables(vma, old_addr, ...);

    return new_addr;
}
VMA 분할과 Maple Tree: munmap()이 VMA의 중간 부분만 해제하면, 기존 VMA가 둘로 분할됩니다. Maple Tree에서는 기존 범위를 축소(pivot 조정)하고 새 VMA를 삽입하는 두 번의 store 연산으로 처리됩니다. RB-Tree 시절에는 rb_erase + rb_insert + 연결 리스트 갱신이 필요했던 것과 대조적입니다.

메모리 컴팩션과 상호작용

메모리 컴팩션(compaction)은 물리 페이지를 재배치하여 연속 빈 페이지를 확보하는 과정입니다. 이 과정에서 VMA 정보를 조회해야 하므로 Maple Tree와 상호작용합니다.

/* mm/compaction.c — 컴팩션 시 VMA 조회 */
static bool suitable_migration_target(
    struct compact_control *cc,
    struct page *page)
{
    struct vm_area_struct *vma;
    struct mm_struct *mm;

    /* 역매핑(rmap)으로 페이지의 VMA 찾기 */
    vma = page_vma(page);
    if (!vma)
        return false;

    mm = vma->vm_mm;

    /* RCU 읽기 경로: Maple Tree에서 VMA 존재 확인
     * 컴팩션은 프로세스 컨텍스트에서 실행되므로
     * mmap_lock을 잡지 않고 RCU 읽기만으로 VMA 조회 가능 */
    rcu_read_lock();
    vma = find_vma(mm, page_address(page));
    /* find_vma() 내부에서 Maple Tree RCU 읽기 수행 */
    rcu_read_unlock();

    return vma != NULL;
}

/* 컴팩션과 Maple Tree의 핵심 상호작용 포인트 */
/*
 * 1. migrate_pages(): 페이지 이동 시 VMA 조회 (RCU 읽기)
 * 2. isolate_migratepages(): 이동 대상 선별 시 VMA 범위 확인
 * 3. compact_zone(): THP 후보 영역 판단 시 VMA 플래그 확인
 *
 * Maple Tree의 RCU lock-free 읽기 덕분에
 * 컴팩션이 mmap_lock 없이도 VMA 정보를 안전하게 조회 가능
 * → 컴팩션과 페이지 폴트가 동시에 진행될 수 있음
 */
컴팩션 단계Maple Tree 연산잠금비고
스캔 (migrate scanner)find_vma()RCU 읽기페이지가 속한 VMA 확인
격리 (isolate)mas_find()RCU 읽기범위 내 VMA 순회
이동 (migrate)직접 사용 안 함page lock페이지 테이블 레벨에서 처리
THP 판단find_vma()RCU 읽기VMA 플래그(VM_HUGEPAGE 등) 확인
컴팩션과 mmap_lock: 메모리 컴팩션은 Maple Tree의 읽기 경로만 사용합니다. VMA를 수정하지 않으므로 mmap_write_lock이 불필요하며, RCU 읽기만으로 충분합니다. 이는 Linux 6.1 이전에는 mmap_read_lock을 잡아야 했던 것과 비교하면, per-VMA lock(6.4+)과 결합하여 컴팩션의 잠금 경합을 크게 줄입니다.

반복자 패턴 상세

Maple Tree는 세 가지 수준의 반복자(iterator) 패턴을 제공합니다. 각각의 사용 사례와 성능 특성을 비교합니다.

mt_for_each() — 간편 순회

/* mt_for_each: 가장 간단한 순회 매크로 */
/* 내부적으로 ma_state를 생성하고 관리 */
#define mt_for_each(__tree, __entry, __index, __max)  \
    for (__entry = mt_find(__tree, &(__index), __max); \
         __entry;                                       \
         __entry = mt_find_after(__tree, &(__index), __max))

/* 사용 예: 전체 VMA 출력 */
void dump_all_vmas(struct mm_struct *mm)
{
    struct vm_area_struct *vma;
    unsigned long idx = 0;

    mmap_read_lock(mm);
    mt_for_each(&mm->mm_mt, vma, idx, ULONG_MAX) {
        pr_info("VMA [%lx-%lx] flags=%lx\n",
                vma->vm_start, vma->vm_end, vma->vm_flags);
    }
    mmap_read_unlock(mm);
}

mas_for_each() — 범위 정보 접근

/* mas_for_each: 범위 정보(mas.index, mas.last)에 접근 가능 */
#define mas_for_each(__mas, __entry, __max)         \
    while ((__entry = mas_find(__mas, __max)) != NULL)

/* 사용 예: 특정 범위 내 VMA와 gap 분석 */
void analyze_address_space(struct mm_struct *mm,
                            unsigned long start, unsigned long end)
{
    MA_STATE(mas, &mm->mm_mt, start, start);
    struct vm_area_struct *vma;
    unsigned long prev_end = start;
    unsigned long total_gap = 0, total_mapped = 0;

    rcu_read_lock();
    mas_for_each(&mas, vma, end) {
        /* gap: prev_end ~ mas.index 사이의 미사용 공간 */
        if (mas.index > prev_end)
            total_gap += mas.index - prev_end;

        /* mapped: mas.index ~ mas.last (= vm_start ~ vm_end-1) */
        total_mapped += mas.last - mas.index + 1;
        prev_end = mas.last + 1;

        pr_info("VMA [%lx-%lx] size=%lu KB\n",
                mas.index, mas.last + 1,
                (mas.last - mas.index + 1) >> 10);
    }
    rcu_read_unlock();

    pr_info("Total: mapped=%lu KB, gap=%lu KB\n",
            total_mapped >> 10, total_gap >> 10);
}

mas_find() / mas_next() — 세밀한 제어

/* mas_find + mas_next: 조건부 순회, 조기 종료 가능 */
void find_executable_vmas(struct mm_struct *mm)
{
    MA_STATE(mas, &mm->mm_mt, 0, 0);
    struct vm_area_struct *vma;
    int count = 0;

    rcu_read_lock();
    /* 첫 번째 VMA 검색 */
    vma = mas_find(&mas, ULONG_MAX);
    while (vma) {
        if (vma->vm_flags & VM_EXEC) {
            pr_info("Exec VMA: [%lx-%lx]\n",
                    vma->vm_start, vma->vm_end);
            count++;
            if (count >= 10)
                break;  /* 최대 10개만 출력 */
        }
        /* 다음 VMA로 이동 */
        vma = mas_next(&mas, ULONG_MAX);
    }
    rcu_read_unlock();
}

/* mas_find_rev + mas_prev: 역방향 순회 */
void find_last_n_vmas(struct mm_struct *mm, int n)
{
    MA_STATE(mas, &mm->mm_mt, ULONG_MAX, ULONG_MAX);
    struct vm_area_struct *vma;

    rcu_read_lock();
    vma = mas_find_rev(&mas, 0);
    while (vma && n-- > 0) {
        pr_info("VMA [%lx-%lx]\n", vma->vm_start, vma->vm_end);
        vma = mas_prev(&mas, 0);
    }
    rcu_read_unlock();
}
패턴내부 상태 관리범위 정보중단/재개RCU 지원사용 사례
mt_for_each자동인덱스만break만내장간단한 전체 순회
mas_for_eachma_stateindex+lastmas_pause()외부범위 분석, 수정 순회
mas_find/nextma_stateindex+last자유외부조건부 순회, 역방향
성능 비교: 세 패턴의 실제 순회 성능은 거의 동일합니다. mt_for_each가 내부적으로 mt_find/mt_find_after를 호출하므로, 차이는 편의성과 제어 수준에 있습니다. 대량 순회(10,000개 이상)에서는 mas_for_eachmas_pause() + cond_resched()를 추가하여 preemption 지연을 방지하세요.

잠금 경합 분석

Maple Tree 도입의 가장 큰 동기 중 하나는 mmap_lock 경합 감소였습니다. mmap_lock과 per-VMA lock의 상호작용을 분석합니다.

mmap_lock 경합 시나리오

mmap_lock 경합: Before vs After Maple Tree Linux 6.0: 전역 mmap_lock 병목 Thread A Thread B Thread C Thread D mmap_lock (R/W) T_A: page_fault → find_vma → handle → unlock T_B: (대기) → page_fault → find_vma → handle T_C: (대기) → (대기) → page_fault → ... 직렬 실행: 처리량 = 1/N Linux 6.4+: per-VMA lock 병렬 Thread A Thread B Thread C Thread D VMA_1 lock VMA_2 lock VMA_3 lock VMA_4 lock T_A: fault on VMA_1 T_B: fault on VMA_2 T_C: fault on VMA_3 T_D: fault on VMA_4 병렬 실행: 처리량 ~ N배 Maple Tree RCU 읽기 → per-VMA lock 획득

잠금 계층 구조

/* 잠금 계층: 넓은 범위 → 좁은 범위 순으로 획득 */
/*
 * Level 0: mmap_write_lock(mm)     — mm 전체 독점 (mmap/munmap/mremap)
 * Level 1: mmap_read_lock(mm)      — mm 전체 공유 읽기 (fallback 경로)
 * Level 2: rcu_read_lock()         — Maple Tree RCU 읽기 (가장 경량)
 * Level 3: vma_start_read(vma)     — 개별 VMA 읽기 잠금 (6.4+)
 * Level 4: mas_lock(&mas)          — Maple Tree 내장 스핀락 (쓰기)
 *
 * 일반적인 경로:
 * - 페이지 폴트 (읽기): Level 2 → Level 3 (Level 1 fallback)
 * - mmap()/munmap():     Level 0 → Level 4
 * - /proc/pid/maps:      Level 1 (또는 Level 2)
 * - 컴팩션:              Level 2
 */

/* 잠금 경합 측정: lockstat 활용 */
/*
 * # echo 1 > /proc/lock_stat
 * # cat /proc/lock_stat | grep mmap
 *
 * class name    con-bounces  contentions  waittime-min  ...
 * mmap_lock-W:       12345        67890       0.50      ...
 * mmap_lock-R:        5678        23456       0.10      ...
 *
 * per-VMA lock 도입 후: mmap_lock-R 경합이 75%+ 감소
 */
잠금범위경합 빈도Maple Tree 관계
mmap_write_lockmm 전체 독점낮음 (mmap/munmap 시)Maple Tree 쓰기 작업 보호
mmap_read_lockmm 전체 공유높음 (폴트 fallback)VMA 조회 + 폴트 처리
rcu_read_lock프리엠션 비활성거의 없음Maple Tree lock-free 읽기
vma_start_read개별 VMA매우 낮음Maple Tree RCU 읽기 후 획득
mas_lock트리 내장 스핀락외부 잠금 사용 시 없음ma_state 연산 보호
mmap_lock 경합 측정 방법: perf lock record -a -- sleep 10perf lock report로 잠금별 경합 시간을 측정할 수 있습니다. Maple Tree + per-VMA lock 환경에서 mmap_lock의 wait-time이 기존 대비 60~80% 감소하는 것이 일반적입니다.

디버깅 심화

CONFIG_DEBUG_MAPLE_TREE 활성화 시 사용 가능한 디버깅 기능과 mt_dump/mt_validate의 내부 동작을 상세히 분석합니다.

mt_dump() 출력 해석

/* mt_dump() 사용법과 출력 해석 */
mt_dump(&mt, mt_dump_hex);

/* 출력 예시 (arange_64 트리, 5개 VMA):
 *
 * maple_arange64 0xffff888100a00000 parent 0xffff888100b00001
 *   height 2 flags 0x05 (ALLOC_RANGE|USE_RCU)
 *
 *   [0] 0xffff888100c00000 [0x0 - 0x7fffffffffff]
 *       maple_arange64 0xffff888100c00000 parent 0xffff888100a00001
 *         [0] 0xffff888100d00000 [0x555555554000 - 0x555555558fff]
 *             → VMA: /usr/bin/app (r-xp)
 *         [1] (nil) [0x555555559000 - 0x55555555bfff]
 *             → gap: 0x3000 bytes
 *         [2] 0xffff888100e00000 [0x55555555c000 - 0x55555555dfff]
 *             → VMA: [heap] (rw-p)
 *         ...
 *       gap[0]=0x0  gap[1]=0x3000  gap[2]=0x0  ...
 *       meta: end=4 gap=1
 *
 * 해석 가이드:
 * - 각 [N]은 slot[N]의 값과 해당 범위
 * - (nil) = NULL slot = 빈 공간 (gap)
 * - gap[N] = slot[N] 서브트리의 최대 연속 빈 공간
 * - meta.end = 마지막 유효 슬롯 인덱스
 * - meta.gap = 최대 gap을 가진 슬롯 인덱스
 */

mt_validate() 내부 검증 항목

/* lib/maple_tree.c — mt_validate()가 검증하는 항목들 */
void mt_validate(struct maple_tree *mt)
{
    /* 1. 루트 포인터 유효성 */
    /* ma_root가 NULL이 아니면 유효한 노드 포인터인지 확인 */

    /* 2. 부모 포인터 일관성 */
    /* 모든 노드의 parent가 실제 부모를 가리키는지 확인 */
    /* parent.slot[i] == this_node 인지 교차 검증 */

    /* 3. pivot 정렬 */
    /* pivot[0] < pivot[1] < ... < pivot[end] 단조 증가 확인 */
    /* pivot[i] >= node_min && pivot[i] <= node_max 범위 확인 */

    /* 4. 슬롯 유효성 */
    /* 내부 노드: slot[i]가 유효한 자식 포인터인지 확인 */
    /* 잎 노드: slot[i]가 유효한 사용자 값 또는 NULL인지 확인 */

    /* 5. gap 배열 일관성 (arange_64만) */
    /* gap[i]가 실제 서브트리의 최대 빈 공간과 일치하는지 확인 */
    /* meta.gap이 실제 최대 gap 위치를 가리키는지 확인 */

    /* 6. 높이 일관성 */
    /* 모든 잎 노드가 동일한 깊이에 있는지 확인 (B-tree 속성) */

    /* 7. 노드 타입 일관성 */
    /* 트리 플래그(ALLOC_RANGE 등)에 맞는 노드 타입 사용 확인 */

    /* 8. 범위 연속성 */
    /* 형제 노드 간 범위가 겹치지 않고 빈틈 없이 연속인지 확인 */

    /* 검증 실패 시 WARN_ON + mt_dump() 자동 출력 */
    if (error)
        WARN_ON(1);
}

CONFIG_DEBUG_MAPLE_TREE 세부 옵션

설정효과성능 영향
CONFIG_DEBUG_MAPLE_TREE=y모든 수정 연산 후 mt_validate() 자동 호출100~1000x 느림
CONFIG_MAPLE_TREE_DEBUG=ymt_dump(), mt_validate() 함수 활성화무시할 수준
CONFIG_LOCKDEP=yMaple Tree 잠금 순서 검증10~30% 느림
CONFIG_PROVE_LOCKING=yRCU 읽기 경로에서 잠금 보유 검증20~50% 느림
# 커널 빌드 시 디버그 옵션 활성화
make menuconfig
# Kernel hacking → Memory Debugging → Debug maple tree

# 또는 .config에 직접 추가
echo 'CONFIG_DEBUG_MAPLE_TREE=y' >> .config
echo 'CONFIG_MAPLE_TREE_DEBUG=y' >> .config
make -j$(nproc)

# 런타임 진단: dmesg에서 maple_tree 관련 경고 확인
dmesg | grep -i 'maple\|mt_validate\|maple_tree'

# KASAN과 함께 사용하면 use-after-free 감지 강화
echo 'CONFIG_KASAN=y' >> .config
# RCU grace period 후 해제된 노드 접근 시 KASAN이 즉시 보고
디버그 빌드 주의사항: CONFIG_DEBUG_MAPLE_TREE는 매 삽입/삭제마다 전체 트리를 O(n)으로 검증하므로, VMA 수가 많은 워크로드에서 시스템이 극도로 느려집니다. 10,000개 VMA를 가진 프로세스의 fork()는 디버그 모드에서 수십 초가 걸릴 수 있습니다. 재현 가능한 버그에만 사용하세요.

Android에서의 Maple Tree

Android는 Linux 커널을 기반으로 하며, GKI(Generic Kernel Image) 5.15+ / 6.1+에서 Maple Tree를 사용합니다. 모바일 환경의 특수한 메모리 관리 요구사항과 Maple Tree의 상호작용을 분석합니다.

Android GKI 버전커널 버전Maple Tree 상태비고
GKI 5.15Linux 5.15미포함RB-Tree 사용
GKI 6.1Linux 6.1포함 (기본)VMA 관리에 사용
GKI 6.6Linux 6.6포함 + per-VMA lock페이지 폴트 병렬화

Android 메모리 압박과 VMA

/* Android 앱의 전형적인 VMA 패턴 */
/*
 * 일반 앱: 300~800개 VMA
 * 대형 앱 (Chrome, Game): 1500~3000개 VMA
 * 시스템 서비스 (system_server): 2000~5000개 VMA
 *
 * Android 특수 VMA:
 * - ashmem (Anonymous Shared Memory) → Maple Tree에서 범위 관리
 * - ION/DMA-BUF 맵핑 → GPU 메모리 공유용 VMA
 * - ART GC 힙 → 다수의 mmap/munmap 호출 발생
 * - Binder 맵핑 → 프로세스 간 통신용 고정 VMA
 */

/* Android Low Memory Killer Daemon (lmkd) — VMA 순회 */
/*
 * lmkd는 /proc/pid/maps를 읽어 프로세스의 메모리 사용량을 추정합니다.
 * Linux 6.1+에서는 Maple Tree의 RCU 읽기 경로를 통해
 * mmap_lock 경합 없이 VMA 정보를 빠르게 순회할 수 있습니다.
 *
 * 메모리 압박이 심한 Android 환경에서
 * lmkd의 VMA 순회가 앱의 페이지 폴트 처리를 차단하지 않음
 * → 사용자 경험(janks) 개선에 직접적으로 기여
 */

Android에서 Maple Tree의 이점

시나리오RB-Tree (GKI 5.15)Maple Tree (GKI 6.1+)개선
앱 시작 (cold start)mmap_lock 경합으로 지연gap 검색 O(log n)10~20% 빠른 시작
ART GC 중 폴트GC 스레드가 mmap_lock 보유per-VMA lock 분리GC stall 감소
앱 전환 (LRU kill)VMA 순회에 mmap_read_lockRCU lock-free 순회jank 프레임 감소
Chrome 탭 (대량 VMA)2000+ VMA에서 find_vma 느림트리 높이 3으로 빠른 검색탭 전환 부드러움
fork (앱 프로세스 생성)VMA 복사 시 잠금 경합벌크 삽입 최적화Zygote fork 가속
Android Zygote 최적화: Android에서 앱 프로세스는 Zygote를 fork()하여 생성됩니다. Zygote는 ART 런타임이 로드된 상태로 1000개 이상의 VMA를 가집니다. Maple Tree의 mas_expected_entries() 벌크 삽입은 이 fork 과정을 가속화하여, 앱 시작 시간에 직접적인 영향을 줍니다.

벤치마킹 방법론

Maple Tree의 삽입, 삭제, 검색 성능을 측정하는 방법과 실측 데이터를 제공합니다.

측정 방법

/* lib/test_maple_tree.c 기반 벤치마크 (커널 모듈) */
static void benchmark_maple_tree(void)
{
    struct maple_tree mt;
    ktime_t start, end;
    int i;

    mt_init_flags(&mt, MT_FLAGS_ALLOC_RANGE);

    /* === 삽입 벤치마크 === */
    start = ktime_get();
    for (i = 0; i < 100000; i++) {
        unsigned long s = i * 0x1000;
        unsigned long e = s + 0xFFF;
        mtree_store_range(&mt, s, e,
                          xa_mk_value(i), GFP_KERNEL);
    }
    end = ktime_get();
    pr_info("Insert 100K: %lld ns/op\n",
            ktime_to_ns(ktime_sub(end, start)) / 100000);

    /* === 검색 벤치마크 === */
    start = ktime_get();
    for (i = 0; i < 100000; i++) {
        unsigned long addr = i * 0x1000 + 0x500;
        mtree_load(&mt, addr);
    }
    end = ktime_get();
    pr_info("Lookup 100K: %lld ns/op\n",
            ktime_to_ns(ktime_sub(end, start)) / 100000);

    /* === gap 검색 벤치마크 === */
    start = ktime_get();
    for (i = 0; i < 10000; i++) {
        MA_STATE(mas, &mt, 0, 0);
        mas_empty_area(&mas, 0, ULONG_MAX, 0x2000);
    }
    end = ktime_get();
    pr_info("Gap search 10K: %lld ns/op\n",
            ktime_to_ns(ktime_sub(end, start)) / 10000);

    /* === 삭제 벤치마크 === */
    start = ktime_get();
    for (i = 0; i < 100000; i++) {
        mtree_erase(&mt, i * 0x1000);
    }
    end = ktime_get();
    pr_info("Delete 100K: %lld ns/op\n",
            ktime_to_ns(ktime_sub(end, start)) / 100000);

    mtree_destroy(&mt);
}

실측 성능 데이터

연산항목 수Maple Tree (ns/op)RB-Tree (ns/op)비율
삽입 (순차)1,0001501200.8x
삽입 (순차)10,0001802001.1x
삽입 (순차)100,0002203501.6x
검색 (랜덤)1,00080901.1x
검색 (랜덤)10,000951501.6x
검색 (랜덤)100,0001102802.5x
삭제10,0001601300.8x
삭제100,0002003101.6x
gap 검색10,0001202,50020.8x
gap 검색100,00014035,000250x
# 유저스페이스 벤치마크 (tools/testing/radix-tree/)
cd tools/testing/radix-tree/
make maple
./maple

# bpftrace로 실시간 레이턴시 히스토그램
bpftrace -e '
kprobe:mtree_store_range {
    @start[tid] = nsecs;
}
kretprobe:mtree_store_range /@start[tid]/ {
    @insert_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}

kprobe:mtree_load {
    @lstart[tid] = nsecs;
}
kretprobe:mtree_load /@lstart[tid]/ {
    @lookup_ns = hist(nsecs - @lstart[tid]);
    delete(@lstart[tid]);
}
' -d 10

# perf로 캐시 미스 비교
perf stat -e cache-misses,cache-references,instructions \
    -p $(pidof target_process) -- sleep 5
벤치마크 해석 주의사항: 위 수치는 특정 하드웨어(Intel Xeon, DDR4)에서의 참고값입니다. 실제 성능은 CPU 캐시 크기, 메모리 대역폭, NUMA 토폴로지에 크게 영향을 받습니다. 특히 항목 수가 적을 때(수백 개) Maple Tree가 RB-Tree보다 약간 느릴 수 있는데, 이는 노드 할당 오버헤드 때문입니다. 항목 수가 많아질수록 캐시 효율 차이가 극대화됩니다.

내부 재균형 알고리즘

Maple Tree의 재균형(rebalancing)은 B-tree의 전통적인 분할/병합에 기반하되, RCU 호환성과 캐시 효율을 위한 특수한 최적화를 포함합니다.

노드 분할 알고리즘 상세

분할 알고리즘: big_node → 좌/우 분할 1. 오버플로 감지: 16/16 슬롯 사용 + 새 항목 삽입 시도 기존 노드의 모든 항목 + 새 항목 → maple_big_node에 임시 저장 (최대 17개) 2. maple_big_node (스택 임시 구조체) pivot[0..16] + slot[0..17] — 정렬된 상태로 모든 항목 보관 3. 분할 지점 결정: mas_split_final_node() 대략 중간 (8/9) 위치, 실제로는 항목 밀도 균형 고려 4a. 좌측 새 노드 big_node[0..split] 복사 4b. 우측 새 노드 big_node[split+1..end] 복사 5. 부모 노드에 새 pivot(=big_node.pivot[split]) 삽입 → 부모도 오버플로 시 연쇄 분할
/* 분할 알고리즘 핵심 — 간략화 */
static int mas_split(struct ma_state *mas,
                      struct maple_big_node *b_node)
{
    unsigned char split_at;
    struct maple_enode *left, *right;

    /* 분할 지점 결정 */
    split_at = b_node->b_end / 2;

    /* 실제로는 더 정교한 휴리스틱:
     * - gap이 큰 쪽을 더 많이 할당 (arange_64)
     * - NULL 슬롯 연속 구간은 한쪽으로 모음
     * - 최소 점유율(MAPLE_MIN_SLOTS) 보장 */
    split_at = mab_calc_split(mas, b_node, &mid_split);

    /* 새 노드 2개 할당 (사전 할당 풀에서) */
    left  = mas_new_ma_node(mas, b_node);
    right = mas_new_ma_node(mas, b_node);

    /* big_node의 [0, split_at]을 left로 복사 */
    mab_mas_cp(b_node, 0, split_at, left, true);

    /* big_node의 [split_at+1, b_end]를 right로 복사 */
    mab_mas_cp(b_node, split_at + 1, b_node->b_end, right, false);

    /* gap 배열 재계산 (arange_64만) */
    if (mt_is_alloc(mas->tree)) {
        mas_update_gap(mas, left);
        mas_update_gap(mas, right);
    }

    /* 부모에 중간 pivot과 두 자식 포인터 삽입 */
    mas_split_parent(mas, left, right,
                      b_node->pivot[split_at]);

    /* 구 노드 RCU 해제 */
    mas_free(mas, old_node);
    return 0;
}

노드 병합 알고리즘

/* 병합(coalesce) 트리거 조건 */
/*
 * 삭제 후 노드의 점유율이 낮아지면 인접 형제 노드와 병합:
 *
 * 조건: node.entries < MAPLE_MIN_SLOTS (약 3~4개)
 *
 * 병합 과정:
 * 1. 현재 노드와 이전/다음 형제 노드의 항목을 합침
 * 2. 합친 결과가 한 노드에 들어가면 → 단순 병합
 * 3. 합친 결과가 초과하면 → 재분배 (rebalance)
 * 4. 부모 노드에서 해당 pivot 제거 → 부모도 희소하면 연쇄 병합
 */

static void mas_coalesce(struct ma_state *mas)
{
    struct maple_enode *node = mas->node;
    struct maple_enode *sibling;
    unsigned char this_end = ma_data_end(node);

    /* 최소 점유율 이상이면 병합 불필요 */
    if (this_end >= MAPLE_MIN_SLOTS)
        return;

    /* 오른쪽 형제와 병합 시도 */
    sibling = mas_next_sibling(mas);
    if (sibling) {
        unsigned char sib_end = ma_data_end(sibling);

        if (this_end + sib_end + 1 <= MAPLE_RANGE64_SLOTS) {
            /* 한 노드에 합칠 수 있음 → 병합 */
            mas_merge_nodes(mas, node, sibling);
        } else {
            /* 초과 → 두 노드 간 재분배 */
            mas_rebalance(mas, node, sibling);
        }
    }

    /* 부모 노드의 점유율도 재검사 */
    mas_ascend(mas);
    mas_coalesce(mas);  /* 재귀적 검사 (실제론 루프) */
}

재균형 트리거 조건 요약

트리거동작노드 할당부모 영향
삽입 → 노드 가득 참분할 (split)2개 새 노드새 pivot 삽입 (연쇄 가능)
삭제 → 점유율 < MIN병합 (coalesce)없음 (해제)pivot 제거 (연쇄 가능)
삭제 → 형제 합산 초과재분배 (rebalance)1~2개 새 노드pivot 값 갱신
루트 자식 1개높이 감소없음 (해제)루트 교체
범위 변경 (축소/확대)pivot 갱신만없음gap 재계산 (arange)
재균형 비용: 분할은 최악의 경우 루트까지 연쇄될 수 있으나, 트리 높이가 낮으므로(대부분 3~5) 실제 연쇄 분할의 비용은 O(log n)입니다. 병합도 마찬가지입니다. Maple Tree의 넓은 팬아웃(10~16) 덕분에 분할/병합 빈도 자체가 RB-Tree의 회전보다 현저히 낮습니다. 100,000개 항목 삽입 시 분할 횟수는 약 6,000~7,000회 (RB-Tree 회전: ~200,000회)입니다.

참고 자료

자료유형설명
docs.kernel.org/core-api/maple_tree.html공식 문서커널 공식 Maple Tree API 문서
LWN: The Maple Tree기사Maple Tree 설계 동기와 초기 구현 설명
LWN: Maple tree ready for review기사Maple Tree 코드 리뷰 과정
lib/maple_tree.c소스핵심 구현 (~7000줄)
include/linux/maple_tree.h소스구조체/매크로 정의
lib/test_maple_tree.c소스커널 셀프 테스트
tools/testing/radix-tree/maple.c소스유저스페이스 테스트

bpftrace 실습 예제

bpftrace를 활용하면 Maple Tree의 내부 동작을 실시간으로 관찰할 수 있습니다. VMA 관련 Maple Tree 연산을 추적하여 삽입/검색/gap 탐색의 빈도와 지연 시간을 측정하고, 트리 재균형 상황을 모니터링할 수 있습니다.

mas_store_gfp 추적: VMA 삽입 모니터링

mas_store_gfp()는 VMA 삽입/갱신 시 호출되는 핵심 함수입니다. mmap, brk, mprotect 등의 시스템 콜 경로에서 이 함수가 트리거됩니다.

# mas_store_gfp 호출 빈도를 프로세스별로 집계
bpftrace -e 'kprobe:mas_store_gfp { @store[comm] = count(); }'

# 출력 예시:
# @store[bash]: 12
# @store[gcc]: 847
# @store[ld]: 2341
# @store[python3]: 156
해석: 링커(ld)나 컴파일러(gcc)처럼 많은 공유 라이브러리를 매핑하는 프로세스가 높은 store 빈도를 보입니다. 컨테이너 시작 시에도 수천 회의 VMA 삽입이 발생합니다.

mas_find 추적: VMA 검색 패턴 분석

# mas_find 호출의 지연 시간 분포 (나노초 단위 히스토그램)
bpftrace -e '
kprobe:mas_find { @start[tid] = nsecs; }
kretprobe:mas_find /@start[tid]/ {
    @find_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}'

# 출력 예시:
# @find_ns:
# [64, 128)         1847 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  |
# [128, 256)         423 |@@@@@@@@                               |
# [256, 512)          31 |                                       |
# [512, 1K)            2 |                                       |

대부분의 검색이 64~128ns 범위에서 완료되며, 이는 Maple Tree의 B-tree 구조 덕분에 2~3회의 노드 접근만으로 대상 VMA를 찾을 수 있기 때문입니다.

mas_empty_area 추적: Gap 검색 모니터링

# mmap 시 빈 공간(gap) 검색 빈도와 지연 시간
bpftrace -e '
kprobe:mas_empty_area { @start[tid] = nsecs; }
kretprobe:mas_empty_area /@start[tid]/ {
    @gap_ns = hist(nsecs - @start[tid]);
    @gap_count = count();
    delete(@start[tid]);
}'

# 출력 예시:
# @gap_count: 5823
# @gap_ns:
# [128, 256)        3241 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  |
# [256, 512)        1892 |@@@@@@@@@@@@@@@@@@@@@@@                |
# [512, 1K)          584 |@@@@@@@                                |
# [1K, 2K)           106 |@                                      |
Gap 검색 성능: maple_arange_64 노드는 서브트리의 최대 gap 크기를 캐싱하므로, 충분한 공간이 없는 서브트리를 즉시 건너뛸 수 있습니다. Gap 검색이 512ns 이상 걸리는 경우는 주소 공간이 심하게 단편화된 상황입니다.

재균형 빈도 모니터링

# 노드 분할(split)과 병합(coalesce) 빈도 추적
bpftrace -e '
kprobe:mas_split { @split[comm] = count(); }
kprobe:mas_rebalance { @rebalance[comm] = count(); }
interval:s:10 { print(@split); print(@rebalance); clear(@split); clear(@rebalance); }'

# 10초 간격 출력 예시:
# @split[gcc]: 47
# @split[ld]: 128
# @rebalance[munmap]: 12

트리 깊이 관찰

# maple_tree의 깊이(ma_meta.depth) 분포 확인
# 커널 tracepoint가 있을 경우 활용, 또는 kprobe 인자 추적
bpftrace -e '
kprobe:mtree_insert_range {
    @tree_ops[comm] = count();
}
interval:s:5 { print(@tree_ops); clear(@tree_ops); }'

# 일반적인 데스크톱 워크로드에서 VMA Maple Tree의 깊이는 3~4입니다.
# 수만 개의 VMA를 가진 대규모 프로세스(예: JVM, Chrome)도 깊이 5를 넘기 어렵습니다.
# 이는 팬아웃 10~16의 B-tree 특성 때문입니다: log16(100000) ≈ 4.2
주의사항: bpftrace로 Maple Tree 내부 함수를 추적할 때, 이 함수들 중 일부는 static inline으로 선언되어 있어 kprobe를 걸 수 없을 수 있습니다. bpftrace -l 'kprobe:mas_*'로 사용 가능한 프로브 포인트를 먼저 확인하세요. 또한 프로덕션 환경에서는 추적 오버헤드가 성능에 영향을 줄 수 있으므로, 짧은 시간만 활성화하는 것을 권장합니다.

rbtree 대비 성능 비교

Linux 6.1에서 VMA 관리가 Red-Black Tree에서 Maple Tree로 전환된 핵심 이유는 성능입니다. 아래는 주요 연산별 비교와 실제 워크로드에서의 영향을 정리합니다.

연산별 벤치마크 비교

아래 수치는 10,000개의 VMA를 가진 프로세스에서 측정한 참고값입니다 (Intel Xeon Platinum 8380, DDR4-3200).

연산RB-TreeMaple Tree개선율핵심 이유
순차 삽입 (10K VMA)1.82 ms1.45 ms~20%회전 대비 분할 빈도 낮음
랜덤 검색 (단일 주소)320 ns185 ns~42%트리 높이 3~4 vs 13~14 (캐시 미스 감소)
범위 검색 (구간 내 VMA 열거)2.1 µs0.8 µs~62%범위 키 네이티브 지원
Gap 검색 (빈 공간 탐색)890 ns280 ns~69%arange_64 augmented gap 캐싱
삭제 (munmap)410 ns350 ns~15%재균형 비용 유사
순회 (전체 VMA)45 µs28 µs~38%연속 메모리 읽기, 프리페치 효과
메모리 사용량 (10K VMA)~560 KB~480 KB~14%별도 구조체(rb_node) 불필요
VMA 수에 따른 경향: VMA 수가 적을 때(수백 개) 두 자료구조의 성능 차이는 미미합니다. VMA 수가 1,000개를 넘어가면서 Maple Tree의 캐시 효율이 발휘되기 시작하고, 10,000개 이상에서는 검색 성능 차이가 2배 이상으로 벌어집니다.

VMA 수에 따른 검색 성능 비교

VMA 수에 따른 랜덤 검색 시간 (ns) 0 100 200 300 400 500 100 1K 5K 10K 50K VMA 수 검색 시간 (ns) 55 105 230 320 460 48 75 125 185 260 RB-Tree Maple Tree

위 차트에서 볼 수 있듯이, VMA 수가 증가할수록 두 자료구조의 성능 격차가 크게 벌어집니다. RB-Tree는 노드당 1개의 키만 저장하므로 트리 높이가 log₂(n)으로 증가하지만, Maple Tree는 팬아웃 10~16 덕분에 log₁₆(n) 수준의 낮은 높이를 유지합니다.

캐시 친화성이 핵심인 이유

특성RB-TreeMaple Tree
노드당 항목 수110~16
10K VMA 트리 높이~14~3
검색 시 캐시 라인 접근14회 (각각 다른 캐시 라인)3~4회 (연속 메모리)
L1 캐시 미스율높음 (포인터 추적)낮음 (연속 배열 스캔)
노드 크기32 bytes (rb_node)256 bytes (캐시 라인 4개 = 정렬됨)
프리페치 효과없음 (랜덤 접근)높음 (배열 순차 접근)

현대 CPU에서 L1 캐시 미스 한 번의 비용은 ~4ns, L2 미스는 ~12ns, L3 미스는 ~40ns입니다. RB-Tree의 14단계 포인터 추적은 거의 매번 L1 캐시 미스를 유발하지만, Maple Tree의 3~4단계 노드 접근은 각 노드 내부에서 pivot 배열을 순차 스캔하므로 하드웨어 프리페처가 효과적으로 동작합니다.

실제 워크로드 영향

워크로드측정 항목개선 효과설명
커널 컴파일 (make -j$(nproc))wall time~1.5% 감소수천 개의 .o 파일 링크 시 mmap/munmap 빈번
PostgreSQL (pgbench TPC-B)TPS~2~3% 향상공유 메모리 매핑, 커넥션별 VMA 관리
컨테이너 시작 (Docker run)startup time~5~8% 감소overlay 파일시스템 매핑으로 VMA 대량 생성
Java (JVM large heap)GC pause 간접 영향측정 가능 수준JVM의 mmap 기반 힙 관리에서 VMA 수 수천~수만
Chrome (탭 100개)메모리 관리 지연~10~15% 감소탭당 수백 VMA, 프로세스당 수만 VMA 도달
fork() 집중 (서버 프로세스)fork latency~8% 감소Maple Tree 복사가 RB-Tree 복사보다 효율적
마이그레이션 투명성: RB-Tree에서 Maple Tree로의 전환은 커널 내부 구현 변경으로, 사용자 공간 API(mmap, munmap, mprotect 등)에는 변화가 없습니다. Linux 6.1 이상으로 커널을 업그레이드하면 자동으로 Maple Tree를 사용하게 됩니다. 별도의 설정이나 애플리케이션 수정은 필요하지 않습니다.
RB-Tree가 여전히 사용되는 곳: VMA 관리는 Maple Tree로 전환되었지만, 커널의 다른 많은 서브시스템(CFS 스케줄러, epoll, ext4 extent 트리 등)에서는 여전히 RB-Tree를 사용합니다. Maple Tree는 범위 기반 데이터에 최적화된 자료구조이므로, 단일 키 기반의 용도에서는 RB-Tree가 여전히 적합합니다.
다음 학습:
  • Red-Black Tree — Maple Tree가 대체한 이전 VMA 자료구조
  • XArray — Maple Tree 구현의 기반이 된 자료구조
  • Interval Tree — 범위 검색의 또 다른 접근법
  • VMA / mmap 심화 — Maple Tree를 활용하는 메모리 관리 상위 레이어
  • RCU — Maple Tree의 동시성 보장 핵심 메커니즘
  • 메모리 관리 개요 — 페이지 테이블, VMA 기초