본문으로 건너뛰기
Abseil Code Review · 29/79

absl::node_hash_map — stable pointer가 필요할 때

· Hawk · 4분 읽기

#한 줄 요약

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가 이동된다.

문제 둘.

  1. value가 크면 이동 비용이 크다.
  2. 외부에서 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_mapnode_hash_mapflat_hash_map
슬롯 storage노드 ptr chain노드 ptr (Swiss)value 직접
value 포인터 안정OOX
iterator 안정 (rehash)OXX
iterator 안정 (insert 다른 키)OOX
lookup cache miss2~3회1~2회0~1회
heterogeneous lookupC++20 부분처음부터처음부터
node_type extract/insertC++17OX

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 probing
if (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_map drop-in 마이그레이션에 적합.
  • 작은 value에서는 flat이 더 빠르다.

#다음 편

Part 5-04 — btree_map에서 sorted 컨테이너의 cache-friendly 대안을 본다.

#관련 항목

Abseil Code Review · 30 of 79

  1. 1 Abseil Code Review — Google production-grade C++ 라이브러리 분석
  2. 2 Abseil 개요 — Google이 std를 보완한 이유
  3. 3 Abseil 설계 철학 — std 호환과 추가 기능의 균형
  4. 4 Abseil 빌드와 의존성 — Bazel vs CMake
  5. 5 Abseil LTS vs HEAD 릴리스 모델 분석
  6. 6 Abseil Versioning과 ABI 호환성 정책
  7. 7 Abseil 매크로 — ABSL_HAVE_*·ABSL_ATTRIBUTE_*
  8. 8 Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
  9. 9 absl::LogSeverity — 로그 레벨 타입
  10. 10 Abseil type_traits — negation·conjunction·void_t
  11. 11 Abseil Conformance·Policy 분석
  12. 12 Abseil Memory utilities 분석
  13. 13 Abseil raw_logging — heap-free 로깅
  14. 14 Abseil thread_annotations — clang TSA 통합
  15. 15 absl::Status — exception-free error handling
  16. 16 absl::StatusOr<T> — 값 또는 에러
  17. 17 absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR
  18. 18 absl::Status payload — 구조화된 에러 컨텍스트
  19. 19 absl::Status ↔ exception 변환 패턴
  20. 20 absl::string_view — non-owning 문자열 참조
  21. 21 absl::string_view 함정 — dangling·c_str·임시 객체
  22. 22 absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
  23. 23 absl::StrSplit — Delimiter·Predicate·컨테이너 변환
  24. 24 absl::StrJoin — 컨테이너 결합과 Formatter
  25. 25 absl::StrFormat — type-safe printf·FormatSpec
  26. 26 Abseil ASCII 함수 — locale-free 분류·대소문자 변환
  27. 27 Abseil Escape — CEscape·HexEscape·Base64
  28. 28 absl::flat_hash_map — Swiss Table 기반 hash map
  29. 29 absl::flat_hash_set — set 버전 Swiss Table
  30. 30 absl::node_hash_map — stable pointer가 필요할 때
  31. 31 absl::btree_map — sorted·cache-friendly B-tree
  32. 32 absl::FixedArray — 런타임 크기 stack 배열
  33. 33 absl::InlinedVector — small buffer optimization
  34. 34 Abseil Swiss Table internals — control byte·SIMD probing
  35. 35 absl::Mutex — reader-writer·fairness·deadlock 검출
  36. 36 absl::Mutex Conditional Critical Section — Await로 cv 없애기
  37. 37 absl::Notification — once-only signal
  38. 38 absl::BlockingCounter·Barrier — 다중 thread 조율
  39. 39 absl::Mutex annotations — clang thread-safety로 race를 컴파일 타임에
  40. 40 absl::Time·Duration 분석 — 단단한 type
  41. 41 absl::Time Format·Parse
  42. 42 absl::CivilTime 분석
  43. 43 absl::time_zone 분석
  44. 44 absl::Time mocking — 테스트 친화 시간
  45. 45 absl::BitGen — 모던 난수 생성기
  46. 46 Abseil Random Distributions — Uniform·Exponential
  47. 47 Abseil Mocking Random — 테스트 결정성
  48. 48 Abseil Random Seeding·Entropy
  49. 49 absl::int128·uint128 분석
  50. 50 absl::bits — popcount·countl_zero
  51. 51 absl::optional vs std::optional
  52. 52 absl::variant 분석
  53. 53 absl::span 분석
  54. 54 absl::any 분석
  55. 55 absl::compare — three-way 비교
  56. 56 Abseil utility — apply·in_place
  57. 57 Abseil AbslHashValue 분석
  58. 58 Abseil HashState chaining
  59. 59 Abseil Custom hashable 구현
  60. 60 Abseil LOG·VLOG·CHECK 분석
  61. 61 Abseil LogSink 분석
  62. 62 Abseil LogEntry·structured logging
  63. 63 Abseil Stack trace·failure_signal_handler
  64. 64 ABSL_FLAG 정의 분석
  65. 65 Abseil ParseCommandLine 동작
  66. 66 Abseil Flag introspection·validation
  67. 67 Google 스타일의 Abseil 사용 패턴
  68. 68 Abseil 자주 보는 anti-pattern
  69. 69 std → absl 마이그레이션 전략
  70. 70 absl::Cleanup — 함수 종료 시 실행 보장
  71. 71 Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
  72. 72 absl::function_ref와 any_invocable — 함수 객체 전달의 두 축
  73. 73 absl::bind_front와 Overload — 함수 객체 보조 도구
  74. 74 absl::Cord — 분산 시스템용 대용량 문자열
  75. 75 absl::from_chars·SimpleAtoi — 빠른 숫자 변환
  76. 76 absl::Cord vs std::string — 선택 기준과 메모리 프로파일
  77. 77 absl::GetStackTrace와 Symbolize — crash 시 readable stack
  78. 78 absl::ComputeCrc32c — 하드웨어 가속 체크섬
  79. 79 absl::PeriodicSampler — 적응형 샘플링·jitter 회피