kernel/data_structures/rbtree.html · kernel/data_structures/rbtree.rst 공식 원문 ↗

Zephyr 3.7.0 · Kernel · Data Structures

Balanced Red/Black Tree

Node당 pointer 두 개만 사용하면서 검색·삽입·삭제를 logarithmic 시간으로 제한하는 allocation-free balanced tree입니다.

Official pathkernel/data_structures/rbtree.html
Source filekernel/data_structures/rbtree.rst
Source versionZephyr 3.7.0
TranslationFull · reviewed

본문 기준: Zephyr 3.7.0 LTS3 · 이후 유지보수 패치는 별도입니다.

Part 1

요약·해설

사용자 비교 predicate로 전체 순서를 정의하고, parent pointer 대신 지역 stack을 만들어 dlist와 같은 node 메모리 overhead로 균형 트리를 구현합니다.

Complexity

O(log2(N))

Node

2 pointers · color bit

Traversal

rb_walk · RB_FOR_EACH

Part 2

접을 수 있는 영어 원문 전체

영어 원문 전체 펼치기
원문 SHA-256 76788BEAFA46B086E9BF87AB6BD26A9C3AD0D5976F66F479D1C30C3E3C78EF8B
.. _rbtree_api:

Balanced Red/Black Tree
=======================

For circumstances where sorted containers may become large at runtime,
a list becomes problematic due to algorithmic costs of searching it.
For these situations, Zephyr provides a balanced tree implementation
which has runtimes on search and removal operations bounded at
O(log2(N)) for a tree of size N.  This is implemented using a
conventional red/black tree as described by multiple academic sources.

The :c:struct:`rbtree` tracking struct for a rbtree may be initialized
anywhere in user accessible memory.  It should contain only zero bits
before first use.  No specific initialization API is needed or
required.

Unlike a list, where position is explicit, the ordering of nodes
within an rbtree must be provided as a predicate function by the user.
A function of type :c:func:`rb_lessthan_t` should be assigned to the
``lessthan_fn`` field of the :c:struct:`rbtree` struct before any tree
operations are attempted.  This function should, as its name suggests,
return a boolean True value if the first node argument is "less than"
the second in the ordering desired by the tree.  Note that "equal" is
not allowed, nodes within a tree must have a single fixed order for
the algorithm to work correctly.

As with the slist and dlist containers, nodes within an rbtree are
represented as a :c:struct:`rbnode` structure which exists in
user-managed memory, typically embedded within the data structure
being tracked in the tree.  Unlike the list code, the data within an
rbnode is entirely opaque.  It is not possible for the user to extract
the binary tree topology and "manually" traverse the tree as it is for
a list.

Nodes can be inserted into a tree with :c:func:`rb_insert` and removed
with :c:func:`rb_remove`.  Access to the "first" and "last" nodes within a
tree (in the sense of the order defined by the comparison function) is
provided by :c:func:`rb_get_min` and :c:func:`rb_get_max`.  There is also a
predicate, :c:func:`rb_contains`, which returns a boolean True if the
provided node pointer exists as an element within the tree.  As
described above, all of these routines are guaranteed to have at most
log time complexity in the size of the tree.

There are two mechanisms provided for enumerating all elements in an
rbtree.  The first, :c:func:`rb_walk`, is a simple callback implementation
where the caller specifies a C function pointer and an untyped
argument to be passed to it, and the tree code calls that function for
each node in order.  This has the advantage of a very simple
implementation, at the cost of a somewhat more cumbersome API for the
user (not unlike ISO C's :c:func:`bsearch` routine).  It is a recursive
implementation, however, and is thus not always available in
environments that forbid the use of unbounded stack techniques like
recursion.

There is also a :c:macro:`RB_FOR_EACH` iterator provided, which, like the
similar APIs for the lists, works to iterate over a list in a more
natural way, using a nested code block instead of a callback.  It is
also nonrecursive, though it requires log-sized space on the stack by
default (however, this can be configured to use a fixed/maximally size
buffer instead where needed to avoid the dynamic allocation).  As with
the lists, this is also available in a :c:macro:`RB_FOR_EACH_CONTAINER`
variant which enumerates using a pointer to a container field and not
the raw node pointer.

Tree Internals
--------------

As described, the Zephyr rbtree implementation is a conventional
red/black tree as described pervasively in academic sources.  Low
level details about the algorithm are out of scope for this document,
as they match existing conventions.  This discussion will be limited
to details notable or specific to the Zephyr implementation.

The core invariant guaranteed by the tree is that the path from the root of
the tree to any leaf is no more than twice as long as the path to any
other leaf.  This is achieved by associating one bit of "color" with
each node, either red or black, and enforcing a rule that no red child
can be a child of another red child (i.e. that the number of black
nodes on any path to the root must be the same, and that no more than
that number of "extra" red nodes may be present).  This rule is
enforced by a set of rotation rules used to "fix" trees following
modification.

.. figure:: ../../../../kernel/data_structures/rbtree.png
    :align: center
    :alt: rbtree example
    :figclass: align-center

    A maximally unbalanced rbtree with a black height of two.  No more
    nodes can be added underneath the rightmost node without
    rebalancing.

These rotations are conceptually implemented on top of a primitive
that "swaps" the position of one node with another in the list.
Typical implementations effect this by simply swapping the nodes
internal "data" pointers, but because the Zephyr :c:struct:`rbnode` is
intrusive, that cannot work.  Zephyr must include somewhat more
elaborate code to handle the edge cases (for example, one swapped node
can be the root, or the two may already be parent/child).

The :c:struct:`rbnode` struct for a Zephyr rbtree contains only two
pointers, representing the "left", and "right" children of a node
within the binary tree.  Traversal of a tree for rebalancing following
modification, however, routinely requires the ability to iterate
"upwards" from a node as well.  It is very common for red/black trees
in the industry to store a third "parent" pointer for this purpose.
Zephyr avoids this requirement by building a "stack" of node pointers
locally as it traverses downward through the tree and updating it
appropriately as modifications are made.  So a Zephyr rbtree can be
implemented with no more runtime storage overhead than a dlist.

These properties, of a balanced tree data structure that works with
only two pointers of data per node and that works without any need for
a memory allocation API, are quite rare in the industry and are
somewhat unique to Zephyr.

Red/Black Tree API Reference
--------------------------------

.. doxygengroup:: rbtree_apis

Part 3

한국어 전문 번역

정렬 container가 runtime에 커질 수 있는 상황에서는 검색의 algorithmic cost 때문에 list가 문제가 됩니다. Zephyr의 balanced tree는 크기가 N인 tree의 검색과 제거 시간을 O(log2(N)) 이내로 제한합니다. 여러 학술 자료에서 설명하는 전통적인 red/black tree 방식으로 구현됩니다.

struct rbtree 추적 구조체는 사용자가 접근할 수 있는 어느 메모리에서나 초기화할 수 있습니다. 처음 사용하기 전에 모든 bit가 0이어야 하며 별도의 초기화 API는 필요하지 않습니다.

위치가 명시적인 list와 달리 rbtree 안 node의 순서는 사용자가 predicate function으로 제공해야 합니다. Tree 연산을 시작하기 전에 rb_lessthan_t 형식 함수를 struct rbtree의 lessthan_fn field에 지정합니다. 이 함수는 tree가 원하는 순서에서 첫 번째 node가 두 번째 node보다 작으면 true를 반환해야 합니다.

동일함을 의미하는 equal 결과는 허용되지 않습니다. Algorithm이 올바르게 동작하려면 tree 안 모든 node에 하나의 고정된 전체 순서가 있어야 합니다.

slist와 dlist처럼 rbtree의 node도 사용자가 관리하는 메모리의 struct rbnode로 표현하며, 보통 tree가 추적할 사용자 구조체 안에 내장합니다. List와 달리 rbnode 내부 데이터는 완전히 opaque합니다. 사용자는 binary tree topology를 꺼내 list처럼 수동으로 순회할 수 없습니다.

rb_insert()로 node를 삽입하고 rb_remove()로 제거합니다. 비교 함수가 정한 순서에서 첫 node와 마지막 node는 rb_get_min()과 rb_get_max()로 얻습니다. rb_contains()는 주어진 node pointer가 tree의 element면 true를 반환합니다. 이 연산들은 모두 tree 크기에 대해 최대 logarithmic time complexity를 보장합니다.

모든 element를 열거하는 방법은 두 가지입니다. rb_walk()는 호출자가 C callback function pointer와 그 함수에 전달할 type 없는 argument를 지정하면 tree code가 순서대로 각 node에 함수를 호출합니다. ISO C bsearch()와 비슷하게 API가 다소 번거롭지만 구현이 단순합니다. 다만 재귀 구현이므로 recursion 같은 제한 없는 stack 기법을 금지하는 환경에서는 항상 사용할 수 있는 것은 아닙니다.

RB_FOR_EACH iterator는 callback 대신 중첩 code block에서 자연스럽게 순회하는 비재귀 방식입니다. 기본적으로 stack에 log 크기 공간이 필요하지만, 필요하면 동적 할당을 피하도록 고정된 최대 크기 buffer를 사용하게 구성할 수 있습니다. RB_FOR_EACH_CONTAINER 변형은 raw node pointer 대신 node를 포함한 container field의 pointer를 제공합니다.

Tree 내부 구조

Zephyr rbtree는 학술 자료에서 널리 설명하는 전통적인 red/black tree입니다. Low-level algorithm은 기존 관례와 같으므로 여기서는 Zephyr 구현에 특이하거나 중요한 점만 다룹니다.

Tree가 보장하는 핵심 불변식은 root에서 어떤 leaf로 가는 경로도 다른 leaf로 가는 경로의 두 배보다 길지 않다는 것입니다. 각 node에 red 또는 black인 color bit 하나를 연관시키고, red node 아래에 red child가 올 수 없다는 규칙을 강제합니다. Root까지 어떤 경로에나 black node 수가 같고 그 수보다 많은 추가 red node가 존재할 수 없게 합니다. 변경 뒤 rotation 규칙으로 tree를 복구하여 이 규칙을 지킵니다.

Black height 2인 최대 불균형 red black tree
Black height가 2인 최대 불균형 rbtree. Rebalancing 없이 가장 오른쪽 node 아래에는 node를 더 추가할 수 없습니다.

Rotation은 개념적으로 한 node의 tree 위치를 다른 node와 바꾸는 primitive 위에서 구현됩니다. 일반 구현은 node 내부의 data pointer만 교환하지만 Zephyr rbnode는 intrusive하므로 그렇게 할 수 없습니다. 교환 대상 하나가 root이거나 둘이 이미 parent/child인 경우 같은 edge case를 처리하는 더 복잡한 코드가 필요합니다.

struct rbnode에는 binary tree 안 node의 left·right child를 나타내는 pointer 두 개만 있습니다. 하지만 변경 뒤 rebalancing에는 node에서 위쪽으로 순회하는 기능도 필요합니다. 많은 red/black tree는 세 번째 parent pointer를 저장하지만, Zephyr는 tree를 아래로 순회하면서 node pointer stack을 지역적으로 만들고 변경에 맞춰 stack을 갱신하여 parent pointer를 피합니다.

따라서 Zephyr rbtree는 dlist와 똑같이 node마다 pointer 두 개만 있으면 됩니다. Node당 pointer 두 개뿐인 balanced tree이면서 memory allocation API가 전혀 필요하지 않은 조합은 흔치 않으며 Zephyr 구현의 독특한 특성입니다.

Red/Black Tree API 참고

API group rbtree_apis은 intrusive balanced tree를 구현합니다. 모든 연산은 O(log2(N))이고 tree 전체 생성·파괴는 amortized O(1)입니다. Runtime memory 사용량을 우선해 size에 최적화되어 있으며 color bit를 pointer와 union하고 parent pointer 대신 순회 중 동적으로 만든 stack을 사용해 node overhead를 pointer 두 개로 제한합니다.

Algorithm과 naming은 기존 학술·교육 구현의 관례를 따릅니다. 참고: Red-black tree.

매크로와 정의

RB_FOR_EACH(tree, node)

재귀 없이 tree를 in-order로 순회합니다. rb_walk()보다 code size 비용은 크지만 C stack 재귀 frame을 피하고 직접 foreach loop를 구성할 수 있습니다. 순회 중 tree를 바꾸면 node가 빠지거나 중복될 수 있으므로 안전하지 않으며 linked list와 달리 _SAFE 변형이 없습니다. Macro가 인자를 여러 번 확장하므로 side effect가 있는 expression을 전달하면 안 됩니다.

매개변수

  • tree: 순회할 struct rbtree pointer
  • node: iterator로 사용할 지역 struct rbnode* variable의 symbol 이름
RB_FOR_EACH_CONTAINER(tree, node, field)

RB_FOR_EACH()와 같지만 node가 struct rbnode를 포함한 임의 형식일 수 있도록 container field logic을 적용합니다.

매개변수

  • tree: 순회할 struct rbtree pointer
  • node: 지역 iterator의 symbol 이름
  • field: node 안의 struct rbnode field 이름

형식 별칭

typedef bool (*rb_lessthan_t)(struct rbnode *a, struct rbnode *b)

Red/black tree 비교 predicate입니다. Tree 정렬 기준에서 A가 B보다 엄격하게 작으면 true, 아니면 false를 반환합니다. 삽입 중 새 node는 항상 A이고 기존 tree node가 B이므로, 원래 같게 비교될 node에 대해 가장 최근 또는 가장 오래전에 추가된 순서를 구현할 때 이 성질을 주의해서 사용할 수 있습니다.

typedef void (*rb_visit_t)(struct rbnode *node, void *cookie)

Node visitor callback prototype입니다.

매개변수

  • node: 방문 중인 node
  • cookie: 사용자가 지정한 data

함수

void rb_insert(struct rbtree *tree, struct rbnode *node)

Node를 tree에 삽입합니다.

void rb_remove(struct rbtree *tree, struct rbnode *node)

Node를 tree에서 제거합니다.

static inline struct rbnode *rb_get_min(struct rbtree *tree)

Tree에서 정렬 순서가 가장 낮은 member를 반환합니다.

static inline struct rbnode *rb_get_max(struct rbtree *tree)

Tree에서 정렬 순서가 가장 높은 member를 반환합니다.

bool rb_contains(struct rbtree *tree, struct rbnode *node)

주어진 node가 tree의 일부면 true를 반환합니다. 내부에서 node pointer를 직접 역참조하지 않고 tree 항목과 pointer equality만 검사합니다. 단, tree의 lessthan callback은 역참조할 수 있습니다. 따라서 pointer 값 자체를 검사하는 set 구조를 구현할 수도 있습니다.

static inline void rb_walk(struct rbtree *tree, rb_visit_t visit_fn, void *cookie)

rbtree를 순회·열거합니다. Code size가 작은 단순 재귀 열거이지만 별도 함수를 제공해야 하고 loop를 일찍 빠져나올 수 없습니다. 반복 구현은 RB_FOR_EACH를 사용합니다.

구조체

struct rbnode

<rb.h>에 정의된 balanced red/black tree node 구조체입니다.

struct rbtree

<rb.h>에 정의된 balanced red/black tree 구조체입니다.

공개 member

struct rbnode *root

Tree의 root node입니다.

rb_lessthan_t lessthan_fn

Tree 안 node를 비교하는 함수입니다.

Source

출처

영어 원문에는 문단, 문서 지시문, 표, 코드, 코드 심벌, 경로를 그대로 보존했습니다.