INTERNAL: Extract hash_tree module from coll_set and coll_map#970
INTERNAL: Extract hash_tree module from coll_set and coll_map#970zhy2on wants to merge 11 commits into
Conversation
a614f6b to
28a1b82
Compare
추가로 남아있는 CLOG 콜백도 hash tree 내부로 가져올 수 있는지 검토하고, 추가로 추출 가능한 부분이 있는지 확인한 뒤 다시 피드백을 요청드리겠습니다. |
9434b3f to
2eef927
Compare
234938c to
8d3450e
Compare
hash_tree 모듈 추출 및 set/map 공통화set과 map 컬렉션에 중복으로 존재하던 hash tree 자료구조 구현을 핵심 설계 원칙hash_tree는 순수 자료구조 레이어로, 아래 책임만 가집니다:
아래는 각 컬렉션(caller) 책임으로 유지합니다:
elem 레이아웃 통일htree_elem_item을 set: 추출된 공통 함수
|
4d4ec29 to
ae839b7
Compare
5587932 to
b5d548c
Compare
namsic
left a comment
There was a problem hiding this comment.
insert/update commit 까지 확인
|
추가 수정 중입니다. 완료된 후 다시 노티 드리도록 하겠습니다. |
3ed4cd1 to
3da6a5b
Compare
|
| map | set | |
|---|---|---|
| sticky check | IS_STICKY_COLLFLG + do_item_sticky_overflowed() |
동일 |
| overflow check | ccnt >= max_map_size |
ccnt >= max_set_size |
| duplicate | htree_elem_link 내부에서 ENGINE_ELEM_EEXISTS |
동일 |
map은 update 경로(do_map_elem_replace_at)가 추가로 존재
delete 경로를 hash_tree 모듈로 교체
htree_elem_unlink 동작
- leaf 탐색:
hcnt[hidx] == -1인 슬롯을 따라 내려가며 삭제할 elem이 있는 leaf node를 찾음. 이 때 merge를 위해 par_node와 par_hidx를 함께 추적 - elem 탐색: leaf의 hash chain을 순회하여 key가 일치하는 elem을 찾음. 없으면 NULL 반환
- elem unlink: chain에서 제거하고 root까지의 모든 조상 node의
tot_elem_cnt감소 - node merge: 삭제 후 node가 비었거나 elem 수가
HTREE_MAX_HASHCHAIN_SIZE / 2미만인 leaf면 child node를 해제하고 elem들을 par_node의 슬롯에 flat chain으로 복귀
htree_elem_unlink_by_cnt 동작
전체 또는 count개만큼 bulk 삭제:
- DFS로 트리를 순회하며 elem을 chain(
unlink한elem들의next포인터로 연결한linked list)으로 수집 - child node가 sparse해지면(
HTREE_MAX_HASHCHAIN_SIZE / 2미만으로 줄어들면 ) 즉시 merge — child node를 유지할 필요 없이 par_node의 슬롯 하나에 flat chain으로 담을 수 있을 만큼 적어진 것이므로 node를 해제하여 메모리를 절약 - 순회 완료 후 root가 비었으면 root node도 해제
map/set get 경로를 hash_tree 모듈로 교체
map vs set get 비교
| map | set | |
|---|---|---|
| 단건 조회 | htree_elem_find (field 기준) |
htree_elem_find (value 기준, exist 체크용) |
| 다건 조회 | htree_elem_find 반복 (field 목록 순회) |
htree_elem_get_rand / htree_elem_unlink_at_offset (랜덤 N개) |
| 전체 조회 | htree_elem_get_by_cnt (count = 0) |
htree_elem_get_by_cnt (count = 0) |
map get 동작
단건 조회 (htree_elem_find)
- field를 key로 hash chain을 탐색하여 일치하는 elem 반환
- delete 옵션이 있으면
htree_elem_unlink기반의do_map_elem_unlink_by_field로 대체
다건 조회 (htree_elem_find 반복)
- 지정한 field 목록을 순회하며 각 field에 대해
htree_elem_find호출 - delete 옵션이 있으면
do_map_elem_unlink_by_field로 대체
전체 조회 (htree_elem_get_by_cnt)
- count=0으로 호출하여 트리 전체를 DFS 순회하며 모든 elem 수집
- delete 옵션이 있으면 unlink=true로 호출하여 수집과 동시에 tree에서 제거
set get 동작
단건 조회 (htree_elem_find)
- value를 key로 hash chain을 탐색하여 존재 여부 확인 (exist 체크용)
다건 조회 (htree_elem_get_rand / htree_elem_unlink_at_offset)
- delete 없음: DFS 순회 중 샘플링 확률로 count개를 수집한 뒤 shuffle
- delete 있음:
htree_elem_unlink_at_offset을 count번 반복 — 매 반복마다 랜덤 offset의 elem을 unlink하며 node merge도 함께 진행
전체 조회 (htree_elem_get_by_cnt)
- count=0, unlink 옵션으로 map과 동일하게 처리
htree_elem_get_by_cnt (unlink=true) vs htree_elem_unlink_by_cnt
htree_elem_get_by_cnt (unlink=true) |
htree_elem_unlink_by_cnt |
|
|---|---|---|
| 반환 방식 | elem_array에 포인터 복사 후 반환 |
elem의 next를 직접 연결해 linked list로 반환 |
| node merge | unlink 후 merge 진행 | unlink 후 merge 진행 |
| 용도 | caller가 array 형태로 후처리 필요할 때 | array 불필요, caller가 linked list로 순회하며 후처리 |
htree_elem_get_by_cnt(unlink=true):elem_array인자를 받아 unlink한 elem 포인터를 순서대로 복사해 담음. node merge는 unlink 과정에서 함께 진행htree_elem_unlink_by_cnt:elem_array인자 없이 unlink한 elem들의next포인터를 직접 조작하여 하나의 linked list로 연결한 뒤 head를 반환. caller가 linked list를 순회하며 각 elem을 후처리
do_htree_range — 공통 범위 순회 함수
htree_elem_get_at_offset, htree_elem_unlink_at_offset, htree_elem_unlink_by_cnt, htree_elem_get_by_cnt 모두 내부적으로 do_htree_range를 공통으로 사용:
- DFS로 hash tree를 순회하며
skip개를 건너뛰고take개를 수집 - slot 단위로
tot_elem_cnt와 skip을 비교하여 slot 전체를 한 번에 건너뜀 — 불필요한 탐색 없이 대상 범위로 바로 이동 unlink옵션으로 수집과 동시에 tree에서 제거 여부 결정- 수집 결과를
elem_array로 반환해야 하는 경우와 linked list로 반환해야 하는 경우가 달라,collect콜백(collect_to_array/collect_to_chain)으로 수집 방식을 분리 do_htree_range는 public API가 아닌 내부 함수이므로, 다소 복잡하더라도 공통 로직을 최대한 통합하는 데 중점을 둠
| 함수 | skip | take | unlink | collect |
|---|---|---|---|---|
htree_elem_get_at_offset |
offset | 1 | false | collect_to_array |
htree_elem_unlink_at_offset |
offset | 1 | true | collect_to_chain |
htree_elem_unlink_by_cnt |
0 | count or all | true | collect_to_chain |
htree_elem_get_by_cnt |
0 | count or all | 옵션 | collect_to_array |
79c2686 to
479a15c
Compare
479a15c to
df63f64
Compare
jhpark816
left a comment
There was a problem hiding this comment.
일부만 리뷰하였고, 모두 리팩토링 관점의 리뷰입니다.
리뷰 의견을 참고하여 다른 부분도 잘 정리 바랍니다.
| break; | ||
| node = (htree_node *)node->htab[hidx]; | ||
| } | ||
| assert(node != NULL && hidx != -1); |
There was a problem hiding this comment.
여기 assert 문은 무조건 만족되는 것이 코드에서 보장되므로 없어도 됩니다.
hidx는 절대 -1이 될 수 없으므로, hidx 변수를 초기에 -1로 설정하지 않아도 됩니다.
오히려 코드를 둔다면, node->hcnt[hidx] == 0 경우에 바로 리턴해도 될 것 같습니다.
while (node != NULL) {
hidx = HTREE_GET_HASHIDX(hval, node->hdepth);
if (node->hcnt[hidx] >= 0)
break;
node = (htree_node *)node->htab[hidx];
}
if (node->hcnt[hidx] == 0) return NULL;| } | ||
|
|
||
| return ENGINE_SUCCESS; | ||
| return do_map_elem_link(info, elem, cookie); |
There was a problem hiding this comment.
insert 하는 경우는 replaced 값을 false 설정해야 할 것 같고,
아래의 코드가 읽기 쉬울 것 같습니다.
map_elem_item *old_elem = (map_elem_item *)htree_elem_find((htree_node *)info->root,
elem->data, elem->nfield,
&map_htree_ops, &pos);
if (old_elem == NULL) {
if (replaced) *replaced = false;
return do_map_elem_link(info, elem, cookie);
}
if (replace_if_exist) {
if (replaced) *replaced = true;
return do_map_elem_replace_at(info, &pos, old_elem, elem);
} else {
return ENGINE_ELEM_EEXISTS;
}There was a problem hiding this comment.
static ENGINE_ERROR_CODE do_map_elem_insert(hash_item *it, map_elem_item *elem,
const bool replace_if_exist, bool *replaced,
const void *cookie)
{
map_meta_info *info = (map_meta_info *)item_get_meta(it);
htree_elem_pos pos;
map_elem_item *old_elem = (map_elem_item *)htree_elem_find((htree_node *)info->root,
elem->data, elem->nfield,
&map_htree_ops, &pos);
if (replaced) *replaced = false;
if (old_elem == NULL)
return do_map_elem_link(info, elem, cookie);
if (!replace_if_exist)
return ENGINE_ELEM_EEXISTS;
ENGINE_ERROR_CODE ret = do_map_elem_replace_at(info, &pos, old_elem, elem);
if (ret == ENGINE_SUCCESS && replaced) *replaced = true;
return ret;
}replaced 초기화를 내부에서 하도록 하고
do_map_elem_replace_at의 성공 여부 확인 후에 replaced = true로 설정하도록 수정하여 반영하였습니다.
| node->tot_elem_cnt += 1; | ||
| } | ||
|
|
||
| static void do_htree_elem_unlink(htree_node **root_pptr, |
There was a problem hiding this comment.
*root 이어도 충분할 것 같은 데, ** 더블 포인터를 사용하였네요.
There was a problem hiding this comment.
do_htree_elem_link, do_htree_elem_unlink 의 인자를 단일 포인터로 변경하였습니다.
| ssize_t *htree_space_delta, | ||
| const void *cookie) | ||
| { | ||
| if (htree_space_delta) *htree_space_delta = 0; |
There was a problem hiding this comment.
htree_space_delta가 NULL인 경우가 없을 것 같습니다.
There was a problem hiding this comment.
기본적으로 htree_elem_link는 element를 트리에 링크하는 역할이고, htree_space_delta는 collection의 space accounting을 위해 추가된 인자이니 caller가 필요하지 않을 수도 있다고 가정하여 NULL을 허용하도록 구현했습니다.
하지만 현재 htree_elem_link / htree_elem_unlink / htree_elem_unlink_at_offset은 항상 caller에서 space_delta를 쓰고 있기 때문에 이 함수들은 NULL을 허용하지 않도록 수정하겠습니다.
| /* traverse to the leaf node that should contain this element */ | ||
| htree_node *node = *root_pptr; | ||
| int hidx; | ||
| while (true) { |
There was a problem hiding this comment.
while (node != NULL) 조건이 낫지 않는 지 ?
There was a problem hiding this comment.
node->hcnt[hidx] < 0이니 자식 노드를 의미하고 NULL이 아님을 보장합니다.
초기에 root가 NULL일 때 조기 종료를 하고 있기 때문에 항상 참이 되는 조건이어서 추가적인 검사 없이 while (true)로 두는 것이 맞아보입니다.
해당 부분은 그대로 두고 일관되지 않았던 htree_elem_find 부분을 수정하였습니다.
|
|
||
| /* split the hash chain into a new child node if it is full */ | ||
| if (!do_htree_node_try_split(&node, &hidx, elem->hval, | ||
| htree_space_delta, cookie)) |
There was a problem hiding this comment.
중복 검사 이후에 바로 try split 하는 것 보다
hcnt[hidx] 검사하여 split 수행하는 것이 나을 것 같습니다.
There was a problem hiding this comment.
hcnt[hidx] 검사가 어떤 검사를 말씀하시는 걸까요?
현재
/* Split the hash chain at *hidx_ptr into a new child node if it is full,
* updating *node_pptr and *hidx_ptr to point to the insertion slot in the child. */
static bool do_htree_node_try_split(htree_node **node_pptr,
int *hidx_ptr, uint32_t hval,
ssize_t *delta, const void *cookie)
{
htree_node *par_node = *node_pptr;
int hidx = *hidx_ptr;
/* chain not full: no split needed */
if (par_node->hcnt[hidx] < HTREE_MAX_HASHCHAIN_SIZE)
return true;
/* split: allocate child, transfer chain, and link as child of par_node */
if (!do_htree_node_split(par_node, hidx, cookie))
return false;do_htree_node_try_split 함수를 들어가면 조건을 먼저 검사한 다음 do_htree_node_split을 호출하도록 되어 있긴 합니다
There was a problem hiding this comment.
try_split을 분리하지 않고, 여기에서 조건 검사 후 node_split 직접 호출하는 방식을 말씀하신 것이 아닌가 합니다.
try_split 구현이 작은 편이고, 호출하는 곳이 여기 뿐이라 저도 같은 코멘트를 달려고 했는데
try_merge와 같은 패턴으로 구현하는 것이 더 유용한가 싶어 언급하지 않았습니다.
| return elem; | ||
| } | ||
|
|
||
| htree_elem_item *htree_elem_get_at_offset(htree_node *node, uint32_t offset) |
There was a problem hiding this comment.
hash_tree 파일에서만 호출되는 것 같습니다.
외부 API로 제공하지 않아도 됩니다.
🔗 Related Issue
⌨️ What I did
insert/update/exist경로를 hash_tree 모듈로 교체htree_elem_link동작space_delta에 반영hcnt[hidx] == -1인 슬롯을 따라 내려가며 삽입할 leaf node를 찾음ENGINE_ELEM_EEXISTS반환HTREE_MAX_HASHCHAIN_SIZE(64)에 도달하면 child node를 새로 할당하고 기존 chain을 child로 이전. 삽입 위치도 child 기준으로 업데이트tot_elem_cnt증가mapvssetinsert 비교두 collection 모두
htree_elem_link호출 전에 동일한 순서로 사전 검사를 수행:IS_STICKY_COLLFLG+do_item_sticky_overflowed()ccnt >= max_map_sizeccnt >= max_set_sizehtree_elem_link내부에서ENGINE_ELEM_EEXISTSmap은 update 경로(
do_map_elem_replace_at)가 추가로 존재delete경로를 hash_tree 모듈로 교체htree_elem_unlink 동작
hcnt[hidx] == -1인 슬롯을 따라 내려가며 삭제할 elem이 있는 leaf node를 찾음. 이 때 merge를 위해 par_node와 par_hidx를 함께 추적tot_elem_cnt감소HTREE_MAX_HASHCHAIN_SIZE / 2미만인 leaf면 child node를 해제하고 elem들을 par_node의 슬롯에 flat chain으로 복귀htree_elem_unlink_by_cnt 동작
전체 또는 count개만큼 bulk 삭제:
unlink한elem들의next포인터로 연결한linked list)으로 수집HTREE_MAX_HASHCHAIN_SIZE / 2미만으로 줄어들면 ) 즉시 merge — child node를 유지할 필요 없이 par_node의 슬롯 하나에 flat chain으로 담을 수 있을 만큼 적어진 것이므로 node를 해제하여 메모리를 절약map/setget 경로를 hash_tree 모듈로 교체mapvssetget 비교htree_elem_find(field 기준)htree_elem_find(value 기준, exist 체크용)htree_elem_find반복 (field 목록 순회)htree_elem_get_rand/htree_elem_unlink_at_offset(랜덤 N개)htree_elem_get_by_cnt(count = 0)htree_elem_get_by_cnt(count = 0)map get 동작
단건 조회 (
htree_elem_find)htree_elem_unlink기반의do_map_elem_unlink_by_field로 대체다건 조회 (
htree_elem_find반복)htree_elem_find호출do_map_elem_unlink_by_field로 대체전체 조회 (
htree_elem_get_by_cnt)set get 동작
단건 조회 (
htree_elem_find)다건 조회 (
htree_elem_get_rand/htree_elem_unlink_at_offset)htree_elem_unlink_at_offset을 count번 반복 — 매 반복마다 랜덤 offset의 elem을 unlink하며 node merge도 함께 진행전체 조회 (
htree_elem_get_by_cnt)htree_elem_get_by_cnt (unlink=true) vs htree_elem_unlink_by_cnt
htree_elem_get_by_cnt (unlink=true)htree_elem_unlink_by_cntelem_array에 포인터 복사 후 반환next를 직접 연결해 linked list로 반환htree_elem_get_by_cnt(unlink=true):elem_array인자를 받아 unlink한 elem 포인터를 순서대로 복사해 담음. node merge는 unlink 과정에서 함께 진행htree_elem_unlink_by_cnt:elem_array인자 없이 unlink한 elem들의next포인터를 직접 조작하여 하나의 linked list로 연결한 뒤 head를 반환. caller가 linked list를 순회하며 각 elem을 후처리do_htree_range — 공통 범위 순회 함수
htree_elem_get_at_offset,htree_elem_unlink_at_offset,htree_elem_unlink_by_cnt,htree_elem_get_by_cnt모두 내부적으로do_htree_range를 공통으로 사용:skip개를 건너뛰고take개를 수집tot_elem_cnt와 skip을 비교하여 slot 전체를 한 번에 건너뜀 — 불필요한 탐색 없이 대상 범위로 바로 이동unlink옵션으로 수집과 동시에 tree에서 제거 여부 결정elem_array로 반환해야 하는 경우와 linked list로 반환해야 하는 경우가 달라,collect콜백(collect_to_array/collect_to_chain)으로 수집 방식을 분리do_htree_range는 public API가 아닌 내부 함수이므로, 다소 복잡하더라도 공통 로직을 최대한 통합하는 데 중점을 둠htree_elem_get_at_offsetcollect_to_arrayhtree_elem_unlink_at_offsetcollect_to_chainhtree_elem_unlink_by_cntcollect_to_chainhtree_elem_get_by_cntcollect_to_array