Abseil Swiss Table internals — control byte·SIMD probing
#한 줄 요약
Swiss Table은 57비트 해시(H1)로 슬롯 그룹 인덱스를, 7비트 해시(H2)로 슬롯 내 빠른 필터링을 하는 open-addressing hash table이다. 핵심은 16개 슬롯의 control byte를 *한 번의 SIMD 명령(SSE2 _mm_cmpeq_epi8 또는 NEON vceqq_u8)*으로 비교해 후보를 찾는 점. CppCon 2017 Matt Kulukundis 강연으로 공개된 이래 abseil, F14(Meta), boost::unordered_flat 등이 채택했다.
#핵심 아이디어
전통 open-addressing의 문제: probing 시 매 슬롯마다 key 비교(메모리 접근 + comparator 호출)가 필요하다. 슬롯이 empty/deleted/full인지도 분리 정보가 필요.
Swiss Table은 control byte라는 메타데이터 배열을 따로 둔다. 각 슬롯 1바이트.
control byte 의미:
- bit 7 = 1 → 빈 슬롯 (empty / deleted / sentinel 구분은 하위 비트)
- bit 7 = 0 → full 슬롯, 하위 7비트 = H2 (해시 일부)
specific values:
0b1xxxxxxx: empty=0b10000000, deleted=0b11111110, sentinel=0b111111110b0xxxxxxx: full, 하위 7비트가 H2
전체 동작을 한눈에 정리하면 다음과 같다.
#H1, H2 분할
64비트 해시를 두 부분으로 나눈다.
64-bit hash = [H1 (57 bits) | H2 (7 bits)].
| 부분 | 용도 |
|---|---|
| H1 | H1 % capacity → 시작 슬롯 그룹 인덱스 |
| H2 | control byte와 비교될 7-bit fingerprint |
H2가 같다고 key가 같다는 보장은 없다 (7비트만 비교). 하지만 다르면 확실히 다르다 — 즉, 빠른 필터다.
#SIMD probing
slots는 그룹 단위로 처리한다. 한 그룹 = 16 슬롯 (SSE2 기준). 검색 흐름:
// 의사 코드size_t probe = H1 % capacity;while (true) { Group g = LoadGroup(ctrl + probe); // 16 ctrl bytes를 SIMD 레지스터로 Mask candidates = g.Match(H2); // H2와 일치하는 비트 마스크 (16비트) for (int i : candidates) { if (key_equal(slot[probe+i].key, key)) return slot[probe+i]; } if (g.HasEmpty()) return end(); // empty 만나면 종료 probe = (probe + GroupSize) & mask;}Group::Match(H2)는 SSE2에서 다음 한 줄이다.
__m128i ctrl = _mm_loadu_si128((__m128i*)ctrl_ptr);__m128i target = _mm_set1_epi8(H2);__m128i eq = _mm_cmpeq_epi8(ctrl, target);uint16_t mask = _mm_movemask_epi8(eq);16개 슬롯을 1 cycle에 비교한다. 후보가 평균 0~1개이므로 실제 key 비교는 거의 없다.
#scalar vs SIMD probe — 그림
이 한 줄 SIMD가 어떤 차이를 만드는지 직관적으로 보면:
scalar 구현은 16번의 직렬 비교가 필요하지만, _mm_cmpeq_epi8는 단일 명령으로 16-way 비교를 수행한다. 결과 비트마스크는 _mm_movemask_epi8로 16-bit 정수로 압축되고 bsf로 첫 매치 인덱스를 뽑는다. 평균 probe 길이를 10~16배 줄이는 핵심 트릭이다.
#NEON / portable fallback
ARM64는 NEON으로 같은 동작이 가능하다.
// 요약uint8x16_t ctrl = vld1q_u8(ctrl_ptr);uint8x16_t target = vdupq_n_u8(H2);uint8x16_t eq = vceqq_u8(ctrl, target);// 16비트 mask 추출은 NEON에 직접 명령이 없어 별도 변환SSE2/NEON이 없는 플랫폼은 portable fallback. 8바이트 그룹, scalar 비교.
// absl/container/internal/raw_hash_set.h (요약 fallback)struct GroupPortableImpl { uint64_t ctrl; BitMask Match(h2_t hash) const { // SWAR (SIMD Within A Register) trick auto x = ctrl ^ (LSBs * static_cast<uint64_t>(hash)); return BitMask((x - LSBs) & ~x & MSBs); }};SWAR(SIMD within a register) bit trick으로 8 슬롯을 한 번에 비교. 진짜 SIMD보다 느리지만 portable.
#tombstone (deleted)
erase가 일어나면 슬롯은 empty가 아니라 deleted(tombstone)이 된다. probing 중 deleted는 건너뛰고 계속, empty는 종료 — 같은 키가 더 뒤에 있을 가능성이 없기 때문.
tombstone이 누적되면 probe 거리가 길어진다. Swiss Table은 임계치(load_factor + tombstone_factor)에 도달하면 rehash_and_grow_if_necessary로 정리한다.
#probing 전략 — quadratic
probe 인덱스는 다음과 같이 증가한다.
size_t probe = H1 % capacity;size_t group_index = 0;while (...) { // 그룹 검사 group_index++; probe = (probe + group_index * GroupSize) & mask;}triangular numbers (1, 3, 6, 10, …) × GroupSize. capacity가 2의 거듭제곱이면 모든 슬롯을 정확히 한 번씩 방문하는 수학적 보장이 있다(triangular number theorem).
#capacity 정책
capacity는 항상 2^k - 1. 마지막 슬롯이 sentinel 역할.
capacity = 15 → ctrl[16] = sentinelcapacity = 31 → ctrl[32] = sentinel이 sentinel이 End() iterator 종료 조건이다.
#load factor
기본 max load factor는 0.875 (7/8). H2가 7비트라 같은 H2 collision이 평균 1/128. 슬롯 그룹 16에서 평균 후보가 거의 0~1.
load_factor를 낮추면 (예: 0.5) lookup이 빨라지지만 메모리 사용이 늘어난다. abseil은 0.875로 균형을 잡는다.
#실제 코드 위치
absl/container/internal/raw_hash_set.h가 핵심 구현. flat_hash_map, flat_hash_set, node_hash_map, node_hash_set이 모두 raw_hash_set<Policy>를 instantiation한다.
// 요약class CommonFields { ctrl_t* ctrl_; // control byte 배열 void* slots_; // 슬롯 배열 size_t size_; size_t capacity_;};
template <typename Policy, ...>class raw_hash_set { CommonFields settings_;
iterator find(const key_type& key) const { auto hash = hash_function()(key); auto seq = probe(ctrl_, hash, capacity_); while (true) { Group g{ctrl_ + seq.offset()}; for (uint32_t i : g.Match(H2(hash))) { if (Policy::apply(EqualElement{key, eq_}, slot_at(seq.offset() + i))) return iterator_at(seq.offset() + i); } if (g.MaskEmpty()) return end(); seq.next(); } }};Policy가 key 추출/비교/이동/소멸을 정의한다. set/map은 같은 코어를 공유하고 Policy만 다르다.
#비교 — F14 (Meta)
Meta의 F14도 같은 아이디어다 — 14비트 fingerprint를 한 cache line(14 슬롯)에 packed. abseil의 H2 7비트보다 충돌 가능성이 낮다(false candidate 적음). 트레이드오프: 슬롯당 메타 크기가 더 크다.
| 항목 | abseil Swiss | Meta F14 |
|---|---|---|
| fingerprint bits | 7 | 14 |
| 그룹 크기 | 16 (SSE2) | 14 (cache line aligned) |
| metadata 위치 | 별도 배열 | 슬롯과 같은 청크 |
두 구현 모두 std::unordered_map보다 압도적으로 빠르다. 워크로드에 따라 미세 차이.
#코드 리뷰 포인트
이 절은 내부 설명이라 직접적인 리뷰 룰은 적다. 다만:
- hash function 품질이 중요하다. H2가 7비트라 hash가 분포가 나쁘면 collision이 폭증.
- load_factor 변경은 잘 모르면 손대지 않는다. abseil 기본이 균형점.
- erase가 많은 워크로드는 tombstone 누적으로 점차 느려진다. 주기적 rehash 검토.
#정리
- Swiss Table = H1(슬롯 인덱스) + H2(7비트 fingerprint) + control byte 그룹.
- 16개 슬롯의 control byte를 SIMD 한 명령으로 비교.
- empty/deleted 분리로 probe 종료 조건이 정확.
- triangular probing으로 모든 슬롯 방문 보장.
- tombstone 누적 시 자동 rehash.
- abseil flat_hash_map/set, node_hash_map/set 모두
raw_hash_set<Policy>위에 있음.
#다음 편
Part 5가 끝났다. Part 6-01 — absl::Mutex에서 동기화 primitive로 넘어간다.
#관련 항목
Abseil Code Review · 34 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_set — set 버전 Swiss Table
Part 5-02: absl::flat_hash_set — flat_hash_map의 set 대응, value-as-key 구조, dedup/membership 워크로드 패턴.
같은 시리즈에서 이어 읽기
absl::flat_hash_map — Swiss Table 기반 hash map
Part 5-01: absl::flat_hash_map — Swiss Table 채택 배경, std::unordered_map 대비 cache locality와 성능, pointer/iterator 안정성 트레이드오프.
같은 시리즈에서 이어 읽기
absl::PeriodicSampler — 적응형 샘플링·jitter 회피
absl::profiling_internal::PeriodicSampler — sampling rate를 동적으로 조정, geometric distribution으로 jitter 회피. 메모리 할당 추적·profiling 인프라의 기반.
같은 시리즈에서 이어 읽기
이 글을 참조하는 글 (7)
- Abseil HashState chaining — Abseil Code Review
- Abseil AbslHashValue 분석 — Abseil Code Review
- absl::bits — popcount·countl_zero — Abseil Code Review
- absl::InlinedVector — small buffer optimization — Abseil Code Review
- absl::node_hash_map — stable pointer가 필요할 때 — Abseil Code Review
- absl::flat_hash_set — set 버전 Swiss Table — Abseil Code Review
- absl::flat_hash_map — Swiss Table 기반 hash map — Abseil Code Review