absl::node_hash_map — stable pointer가 필요할 때
#한 줄 요약
absl::node_hash_map은 Swiss Table 위에 노드 indirection을 얹은 변형이다. flat의 cache 친화 lookup을 유지하면서 value pointer/reference 안정성을 보장한다. std::unordered_map을 그대로 대체하려는 경우의 drop-in이며, flat_hash_map의 rehash 비용이 부담스러운 큰 value에 적합하다.
#동기
flat_hash_map은 value를 슬롯에 직접 저장한다. rehash가 일어나면 모든 value가 이동된다.
문제 둘.
- value가 크면 이동 비용이 크다.
- 외부에서 value 주소를 보관할 수 없다 — rehash 후 주소가 바뀐다.
std::unordered_map은 이 두 문제를 노드 기반 설계로 해소한다. value는 별도 heap 노드에, 슬롯은 노드 포인터만 갖는다. node_hash_map은 Swiss Table에 같은 인디렉션 전략을 도입했다.
#API와 사용법
#include "absl/container/node_hash_map.h"
absl::node_hash_map<std::string, BigStruct> m;m.emplace("key", BigStruct{});
BigStruct* p = &m["key"];m.emplace("other", BigStruct{}); // rehash 가능// p는 여전히 유효 — value 노드가 별도 heap에 있음인터페이스는 flat_hash_map과 동일하다. 차이는 포인터/참조 안정성뿐.
#안정성 보장
node_hash_map은 다음을 보장한다.
- value의 포인터/참조: insert, erase, rehash에도 안정.
- value의 iterator: rehash에서 무효화될 수 있다 (
flat동일).
iterator와 포인터의 안정성이 다르다는 점이 미묘하다. 포인터를 들고 있어도 iterator로 재방문할 때는 새로 find를 거쳐야 한다.
absl::node_hash_map<std::string, BigStruct> m;auto it = m.emplace("key", BigStruct{}).first;BigStruct* p = &it->second;
m.emplace("other", BigStruct{});
// iterator 위험// *it; // UB 가능 (rehash)
// 포인터는 안전*p; // OK#std::unordered_map vs node_hash_map vs flat_hash_map
| 항목 | unordered_map | node_hash_map | flat_hash_map |
|---|---|---|---|
| 슬롯 storage | 노드 ptr chain | 노드 ptr (Swiss) | value 직접 |
| value 포인터 안정 | O | O | X |
| iterator 안정 (rehash) | O | X | X |
| iterator 안정 (insert 다른 키) | O | O | X |
| lookup cache miss | 2~3회 | 1~2회 | 0~1회 |
| heterogeneous lookup | C++20 부분 | 처음부터 | 처음부터 |
node_type extract/insert | C++17 | O | X |
node_hash_map이 정확히 unordered_map의 안정성 — value pointer만 — 을 보장하면서 hash 구조는 Swiss Table을 쓴다.
#내부 구현
node_hash_map은 슬롯에 pair<const K, V>*를 저장한다. flat의 slot 크기는 (K, V) 직접이지만, node의 slot은 8바이트 포인터다. 슬롯이 작아 cache line당 더 많은 슬롯을 담을 수 있다. lookup은 다음과 같다.
// 의사 코드slot = FindSlot(hash); // Swiss Table probingif (slot->ptr == nullptr) return end();if (slot->ptr->first == key) return slot->ptr;// 다음 슬롯 ...key 비교 시 한 번의 indirection이 필요하다 — 슬롯의 포인터를 deref해 노드의 key를 읽는다. flat은 슬롯에서 바로 key를 읽는다. 그래서 lookup은 flat이 더 빠르다. 다만 노드 크기가 작아 probing 범위가 좁고, 큰 value의 rehash 비용이 없다는 점이 상쇄.
#코드 리뷰 포인트
1. value 주소 노출 API
class Cache { public: BigStruct* Get(absl::string_view key); // 반환 포인터 lifetime 보장 필요 private: absl::node_hash_map<std::string, BigStruct> map_;};API가 value 주소를 반환하면 flat은 위험하다. node를 쓰거나 value를 unique_ptr<BigStruct>로 래핑.
2. 큰 value type
value sizeof가 32B 이상이면 node가 보통 빠르다. rehash 빈도, value 이동 비용을 같이 고려.
대략 가이드:
- value < 16B → flat
- 16B ≤ value < 64B → 워크로드에 따라
- value ≥ 64B → node 또는
unique_ptr
3. unordered_map 마이그레이션
// 회피 — 안정성 가정이 깨질 수 있음std::unordered_map<K, V> → absl::flat_hash_map<K, V>
// 안전 — 동일 안정성std::unordered_map<K, V> → absl::node_hash_map<K, V>마이그레이션 시 pointer-stability 가정에 의존하는 코드가 있으면 node로 먼저 옮기고, 점진적으로 flat을 검토한다.
#안티패턴
모든 hash map을 node로 통일
이는 unordered_map의 비효율을 그대로 복제한다. 안정성이 필요하지 않은 코드에서는 flat이 정답.
iterator 안정성 가정
auto it = m.find("key");m.emplace(...); // rehash 가능LOG(INFO) << it->second; // 위험 — node도 iterator는 무효화포인터만 안정하다. iterator는 보관하지 말 것.
node_hash_set과의 일관성
absl::node_hash_set도 동일 트레이드오프로 존재한다. 큰 element + 포인터 안정성 + Swiss Table 구조가 필요하면 사용.
#정리
node_hash_map은 Swiss Table + 노드 indirection.- value 포인터/참조 안정 보장, iterator는 rehash에서 무효화.
- 큰 value, pointer-stability API,
unordered_mapdrop-in 마이그레이션에 적합. - 작은 value에서는 flat이 더 빠르다.
#다음 편
Part 5-04 — btree_map에서 sorted 컨테이너의 cache-friendly 대안을 본다.
#관련 항목
Abseil Code Review · 30 of 79
- 1 Abseil Code Review — Google production-grade C++ 라이브러리 분석
- 2 Abseil 개요 — Google이 std를 보완한 이유
- 3 Abseil 설계 철학 — std 호환과 추가 기능의 균형
- 4 Abseil 빌드와 의존성 — Bazel vs CMake
- 5 Abseil LTS vs HEAD 릴리스 모델 분석
- 6 Abseil Versioning과 ABI 호환성 정책
- 7 Abseil 매크로 — ABSL_HAVE_*·ABSL_ATTRIBUTE_*
- 8 Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
- 9 absl::LogSeverity — 로그 레벨 타입
- 10 Abseil type_traits — negation·conjunction·void_t
- 11 Abseil Conformance·Policy 분석
- 12 Abseil Memory utilities 분석
- 13 Abseil raw_logging — heap-free 로깅
- 14 Abseil thread_annotations — clang TSA 통합
- 15 absl::Status — exception-free error handling
- 16 absl::StatusOr<T> — 값 또는 에러
- 17 absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR
- 18 absl::Status payload — 구조화된 에러 컨텍스트
- 19 absl::Status ↔ exception 변환 패턴
- 20 absl::string_view — non-owning 문자열 참조
- 21 absl::string_view 함정 — dangling·c_str·임시 객체
- 22 absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
- 23 absl::StrSplit — Delimiter·Predicate·컨테이너 변환
- 24 absl::StrJoin — 컨테이너 결합과 Formatter
- 25 absl::StrFormat — type-safe printf·FormatSpec
- 26 Abseil ASCII 함수 — locale-free 분류·대소문자 변환
- 27 Abseil Escape — CEscape·HexEscape·Base64
- 28 absl::flat_hash_map — Swiss Table 기반 hash map
- 29 absl::flat_hash_set — set 버전 Swiss Table
- 30 absl::node_hash_map — stable pointer가 필요할 때
- 31 absl::btree_map — sorted·cache-friendly B-tree
- 32 absl::FixedArray — 런타임 크기 stack 배열
- 33 absl::InlinedVector — small buffer optimization
- 34 Abseil Swiss Table internals — control byte·SIMD probing
- 35 absl::Mutex — reader-writer·fairness·deadlock 검출
- 36 absl::Mutex Conditional Critical Section — Await로 cv 없애기
- 37 absl::Notification — once-only signal
- 38 absl::BlockingCounter·Barrier — 다중 thread 조율
- 39 absl::Mutex annotations — clang thread-safety로 race를 컴파일 타임에
- 40 absl::Time·Duration 분석 — 단단한 type
- 41 absl::Time Format·Parse
- 42 absl::CivilTime 분석
- 43 absl::time_zone 분석
- 44 absl::Time mocking — 테스트 친화 시간
- 45 absl::BitGen — 모던 난수 생성기
- 46 Abseil Random Distributions — Uniform·Exponential
- 47 Abseil Mocking Random — 테스트 결정성
- 48 Abseil Random Seeding·Entropy
- 49 absl::int128·uint128 분석
- 50 absl::bits — popcount·countl_zero
- 51 absl::optional vs std::optional
- 52 absl::variant 분석
- 53 absl::span 분석
- 54 absl::any 분석
- 55 absl::compare — three-way 비교
- 56 Abseil utility — apply·in_place
- 57 Abseil AbslHashValue 분석
- 58 Abseil HashState chaining
- 59 Abseil Custom hashable 구현
- 60 Abseil LOG·VLOG·CHECK 분석
- 61 Abseil LogSink 분석
- 62 Abseil LogEntry·structured logging
- 63 Abseil Stack trace·failure_signal_handler
- 64 ABSL_FLAG 정의 분석
- 65 Abseil ParseCommandLine 동작
- 66 Abseil Flag introspection·validation
- 67 Google 스타일의 Abseil 사용 패턴
- 68 Abseil 자주 보는 anti-pattern
- 69 std → absl 마이그레이션 전략
- 70 absl::Cleanup — 함수 종료 시 실행 보장
- 71 Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
- 72 absl::function_ref와 any_invocable — 함수 객체 전달의 두 축
- 73 absl::bind_front와 Overload — 함수 객체 보조 도구
- 74 absl::Cord — 분산 시스템용 대용량 문자열
- 75 absl::from_chars·SimpleAtoi — 빠른 숫자 변환
- 76 absl::Cord vs std::string — 선택 기준과 메모리 프로파일
- 77 absl::GetStackTrace와 Symbolize — crash 시 readable stack
- 78 absl::ComputeCrc32c — 하드웨어 가속 체크섬
- 79 absl::PeriodicSampler — 적응형 샘플링·jitter 회피
관련 글
absl::flat_hash_map — Swiss Table 기반 hash map
Part 5-01: absl::flat_hash_map — Swiss Table 채택 배경, std::unordered_map 대비 cache locality와 성능, pointer/iterator 안정성 트레이드오프.
같은 시리즈에서 이어 읽기
Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
absl::c_* algorithm wrapper — container 전체를 받아 begin/end 자동 처리, STL algorithm의 한 줄 boilerplate를 제거.
같은 시리즈에서 이어 읽기
absl::InlinedVector — small buffer optimization
Part 5-06: absl::InlinedVector — std::vector + 작으면 stack, 커지면 heap. SBO 패턴의 표준 도구.
같은 시리즈에서 이어 읽기