Design Principles

July 1, 2026 · View on GitHub

Collision Resolution (varies by version)

VersionStrategyHigh LF Support
emhash5Three-way hybrid: linear probing → quadratic probing → bidirectional searchWith EMH_HIGH_LOAD
emhash6Linked-bucket with separate bitmask for fast empty-bucket searchNative (0.80-0.999 via max_load_factor())
emhash7Linked-bucket with separate bitmask, no tombstonesNative (0.80-0.999)
emhash8Separate index + dense pairs array, linked-bucket chainsWith EMH_HIGH_LOAD

In emhash/hash_table5.hpp, the optimized three-way linear probing strategy is still 2-3x faster than traditional strategies even at load factors > 0.9.

Memory Layout

emhash5/6/7 — Single inline array with embedded bucket linkage:

struct Entry {
    Key key;           // Key
    size_t bucket;     // Bucket chain linkage / state info
    Value value;       // Value
};
// _pairs[bucket] stores key, value, and chain pointer in one struct
// emhash6/7 additionally use a separate _bitmask for fast empty-bucket search

emhash8 — Split index + dense pairs (like std::vector):

struct Index { size_t next, slot; };  // Per-bucket chain linkage
Index*    _index;   // bucket → slot mapping
value_type* _pairs; // dense, compact key-value array (no metadata)
// _pairs is always packed: _pairs[0].._pairs[_num_filled-1]
// This enables extremely fast iteration (just scan _pairs sequentially)

Primary Bucket Mapping

  • Primary bucket is always assigned to key_hash(key) % size and cannot be occupied
  • All operations start searching from the primary bucket
  • Avoids circular displacement issues in cuckoo hashing

Backup Hash Function

Enable backup hash function by defining EMH_SAFE_HASH macro to defend against hash attacks (performance cost ~ 10%).


Implementation Comparison

emhash provides 4 different implementations, each with different focus:

ImplementationLayoutCollision StrategyDefault LFBest Scenario
emhash5Inline _pairs[]Three-way hybrid probing0.80Fast lookup/erase with integer keys, SBO support
emhash6Inline _pairs[] + _bitmaskLinked-bucket0.80Lookup/erase with integer keys, fast empty scan
emhash7Inline _pairs[] + _bitmaskLinked-bucket, no tombstones0.80High load factor (0.80-0.999), insert-intensive
emhash8Separate _index[] + dense _pairs[]Linked-bucket0.80Complex keys/values, extremely fast iteration

Selection Guide

  • Complex/large keys/values (e.g., std::string, custom structs) → emhash8
  • Insert-intensive workloads or need high load factor → emhash7
  • Fast search and erase with integer keysemhash5/6
  • Small maps that should avoid heap allocationemhash5 with EMH_SMALL_SIZE

HashSet

emhash also provides HashSet implementations. Note that HashSet and HashMap use independent version numberinghash_set2/3/4 are not derived from hash_table2/3/4 (which don't exist). The mapping is:

HashSetUnderlying DesignDescription
hash_set2.hppOpen addressing with inline bucket linkageBasic hash set
hash_set3.hppOpen addressing with inline bucket linkageAlternative probing strategy
hash_set4.hppOpen addressing with inline bucket linkageEnhanced version
hash_set8.hppSeparate index + dense key arrayFast iteration, complex key support
#include "emhash/hash_set2.hpp"   // namespace emhash2
#include "emhash/hash_set3.hpp"   // namespace emhash3
#include "emhash/hash_set4.hpp"   // namespace emhash4
#include "emhash/hash_set8.hpp"   // namespace emhash8

emhash2::HashSet<int> set1;
emhash3::HashSet<int> set2;
emhash4::HashSet<int> set3;
emhash8::HashSet<int> set4;

Custom Allocator

All HashMap versions support custom allocators (emhash5/6/7/8):

emhash7::HashMap<Key, Val, Hash, Eq, MyAllocator> mymap;

emilib Library: SIMD-Accelerated Hash Maps

The emilib library provides three hash map implementations (emihmap1, emihmap2, emihmap3) that share a common design philosophy: SIMD-accelerated open addressing with metadata-byte filtering. Unlike emhash's linked-bucket approach, emilib uses Swiss-table-style byte-level metadata to enable vectorized lookups, achieving extremely fast search performance on modern CPUs.

Common Design Foundation

All three implementations share these core principles:

1. Dual-Hash Metadata Encoding

Each key produces two hash values from a single hash computation:

hash = hasher(key)
H1 = hash & _mask          // primary bucket index (aligned to SIMD group)
H2 = (hash % 253) + 3      // 1-byte metadata tag (range [3..255])
  • H1 determines the starting SIMD group for probing
  • H2 is stored as a 1-byte tag in the _states[] array, enabling SIMD-filtered lookup before key comparison

2. SIMD Group Probing

Buckets are organized in groups of simd_bytes (16 for SSE2, 32 for AVX2, 64 for AVX-512). A single SIMD instruction compares 16/32/64 metadata bytes simultaneously:

// Pseudocode: find key in hash map
vec = SIMD_LOAD(&_states[group_start])       // load 16 metadata bytes
mask = SIMD_MOVEMASK(SIMD_CMPEQ(vec, H2))    // find matching H2 tags
for each set_bit in mask:
    if key == _pairs[bucket].first: return bucket  // verify full key

This reduces key comparisons by ~93.75% (15/16 buckets filtered by H2 mismatch on SSE2).

3. State Encoding

Each bucket has a 1-byte state tag in _states[]:

StateValueMeaning
EEMPTY-128 (0x80)Never occupied
EDELETE-127 (0x81)Was occupied, now deleted (tombstone)
EFILLED+3..255Occupied; value = H2 tag

The sentinel value ESENTINEL (127) marks the end of the _states array.

4. Data Layout

_states[]:  [H2][H2][EMP][DEL][H2]...[SENTINEL]  ← metadata (1 byte/bucket)
_pairs[]:   [KV][KV][   ][   ][KV]...            ← key-value pairs

_pairs[i] and _states[i] share the same bucket index, enabling direct indexing.


emihmap1: Group-Probing with Inline Probe Depth

Namespace: emilib | File: emilib/emihmap1.hpp

Core Design

emihmap1 uses a group-based probing strategy where the probe depth is stored inline within the _states array itself. Each SIMD group's last byte encodes the maximum probe offset for that group, eliminating the need for a separate offset array.

Internal Structure

class HashMap {
    HashT   _hasher;
    EqT     _eq;
    int8_t* _states;          // metadata + inline probe depth
    PairT*  _pairs;           // key-value storage
    size_t  _num_buckets;     // total bucket count (power of 2)
    size_t  _mask;            // _num_buckets - 1
    size_t  _num_filled;      // number of occupied entries
};
// Max load factor: 5/6 ≈ 0.833 (hardcoded MXLOAD_FACTOR = 5)

Key Mechanism: Inline Group Probe Depth

In each SIMD group of 16 buckets, 15 slots store H2 tags and 1 slot (the group_index position) stores the probe depth for that group:

Group layout (16 bytes):
[H2][H2][H2]...[H2][PROBE_DEPTH]  ← 15 data slots + 1 probe depth slot
 ^--- slot_size = 15 ---^  ^-- group_index = 15
// Read probe depth for a group
int group_probe(size_t gbucket) const {
    return _states[gbucket + group_index];  // last byte of group
}

// Set probe depth when inserting
void set_group_probe(size_t gbucket, int offset) {
    if (offset > _states[gbucket + group_index])
        _states[gbucket + group_index] = offset;
}

Lookup Algorithm

find_filled_bucket(key):
    H2 = hash_key2(main_bucket, key)   // get H1 (aligned) + H2
    next_bucket = main_bucket
    offset = 0
    while true:
        vec = SIMD_LOAD(&_states[next_bucket])
        mask = MOVEMASK(CMPEQ(vec, H2)) & group_bmask  // filter H2 matches
        for each match in mask:
            if _pairs[slot].first == key: return slot    // full key compare
        if ++offset > group_probe(main_bucket):          // check inline depth
            return NOT_FOUND
        next_bucket = get_next_bucket(next_bucket, offset)

Erase Strategy

When erasing, the bucket state is set to EEMPTY or EDELETE based on whether the group still contains other filled entries:

_erase(bucket):
    _pairs[slot].~PairT()
    _states[bucket] = group_has_empty(bucket) ? EEMPTY : EDELETE

This optimization allows the search to terminate early when encountering EEMPTY (no need to probe further), while EDELETE indicates potential collisions beyond.

Performance Characteristics

OperationAverageWorst Case
LookupO(1) ~1-2 group probesO(n) pathological hash
InsertO(1) amortizedO(n) + rehash
EraseO(1)O(n)
IterationO(n/filled_ratio) with SIMDO(n)
Space overhead1 byte/bucket (states)~6.25% for pairs

Key Optimizations

  • Zero extra offset storage: Probe depth embedded in _states[] group trailer byte
  • Early termination: EEMPTY state allows search to stop without probing all groups
  • SIMD iterator: filled_mask() uses SIMD to skip empty groups during iteration
  • Prefetch: _mm_prefetch on the pair data when H2 matches are found

emihmap2: PSL-Based Probing with Separate Offset Array

Namespace: emilib2 | File: emilib/emihmap2.hpp

Core Design

emihmap2 uses a Probe Sequence Length (PSL) approach with a separate _offset[] array that stores the maximum probe distance for each primary bucket. This decouples metadata from probe tracking, allowing all 16 bytes per SIMD group to store H2 tags (no group trailer byte wasted).

Internal Structure

class HashMap {
    HashT    _hasher;
    EqT      _eq;
    int8_t*  _states;          // metadata only (all 16 bytes = H2 tags)
    uint8_t* _offset;          // per-group probe depth (1 byte per OFFSET_STEP buckets)
    PairT*   _pairs;           // key-value storage
    size_t   _num_buckets;
    size_t   _mask;
    size_t   _num_filled;
    uint32_t _mlf;             // load factor as fixed-point (1<<28 / lf)
};
// Default load factor: 0.84, configurable 0.25..0.999

Key Mechanism: Separate Offset Array

The _offset[] array stores the maximum probe distance per group of OFFSET_STEP (default 8) buckets:

// Read max probe distance for a primary bucket
uint32_t get_offset(main_bucket) const {
    return _offset[main_bucket / OFFSET_STEP];
}

// Update probe distance when inserting further than before
void set_offset(main_bucket, off) {
    _offset[main_bucket / OFFSET_STEP] = off;
}

With EMH_SAFE_PSL, offsets > 128 are encoded as 128 + off/128, supporting probe distances up to 16,384.

Probing Strategy

emihmap2 supports multiple probing strategies via compile-time macros:

get_next_bucket(next_bucket, offset):
    #if EMH_PSL_LINEAR
        return (next_bucket + 3 * simd_bytes) & _mask     // fixed-step linear
    #elif offset < 5
        return (next_bucket + simd_bytes * offset) & _mask // quadratic-like
    #else
        return (next_bucket + _num_buckets/11 + 1) & _mask // jump probing

Lookup Algorithm

find_filled_bucket(key):
    H2 = hash_key2(main_bucket, key)
    next_bucket = main_bucket

    // First group: check H2 matches AND early-empty termination
    vec = SIMD_LOAD(&_states[next_bucket])
    mask = MOVEMASK(CMPEQ(vec, H2))
    for each match: if key match: return bucket
    if MOVEMASK(CMPEQ(vec, simd_empty)): return NOT_FOUND  // early exit!
    if get_offset(main_bucket) == 0: return NOT_FOUND       // no probes yet

    // Subsequent groups
    do:
        next_bucket = get_next_bucket(next_bucket, ++offset)
        vec = SIMD_LOAD(&_states[next_bucket])
        mask = MOVEMASK(CMPEQ(vec, H2))
        for each match: if key match: return bucket
    while offset <= get_offset(main_bucket)
    return NOT_FOUND

Erase Strategy

With EMH_PSL_LINEAR and EMH_PSL_ERASE, emihmap2 can perform backward deletion — when a bucket is cleared and the next bucket is empty, it backtracks to convert EDELETE markers to EEMPTY, improving future lookup performance:

_erase(bucket):
    _pairs[bucket].~PairT()
    #if EMH_PSL_LINEAR
        _states[bucket] = (_states[bucket+1] == EEMPTY) ? EEMPTY : EDELETE
    #else
        _states[bucket] = EDELETE
    #endif

    #if EMH_PSL_ERASE && EMH_PSL_LINEAR
        if _states[bucket] == EEMPTY:
            bucket = (bucket - 1) & _mask
            while _states[bucket] == EDELETE:
                _states[bucket] = EEMPTY
                _offset[bucket/OFFSET_STEP] = 0
                bucket = (bucket - 1) & _mask
    #endif

Performance Characteristics

OperationAverageWorst Case
LookupO(1) ~1-2 group probesO(n) pathological hash
InsertO(1) amortizedO(n) + rehash
EraseO(1), backward deletion O(k)O(n)
IterationO(n/filled_ratio) with SIMDO(n)
Space overhead1 byte/bucket (states) + 1 byte/OFFSET_STEP buckets (offset)~6.3%

Key Optimizations

  • Full SIMD utilization: All 16 bytes per group store H2 tags (no probe depth byte)
  • Early empty detection: First group checks for EEMPTY to terminate search immediately
  • Configurable probing: Linear, quadratic, or jump probing via compile-time macros
  • Backward deletion: Converts tombstones back to empty, reducing probe lengths
  • AVX2/AVX-512 support: Runtime-selectable SIMD width (SSE2/AVX2/AVX-512)

emihmap3: Global PSL with Adaptive Probing

Namespace: emilib3 | File: emilib/emihmap3.hpp

Core Design

emihmap3 uses a single global _max_probe_length variable instead of per-group offset tracking. This simplifies the data structure at the cost of less precise search termination. It also uses a group-level empty check (group_mask) that stores a summary byte per SIMD group.

Internal Structure

class HashMap {
    HashT    _hasher;
    EqT      _eq;
    int8_t*  _states;           // metadata + group summary bytes
    PairT*   _pairs;            // key-value storage
    size_t   _num_buckets;
    size_t   _mask;
    size_t   _num_filled;
    size_t   _max_probe_length; // global maximum probe distance
    uint32_t _mlf;              // load factor as fixed-point
};
// Default load factor: 0.84, configurable 0.25..0.999

Key Mechanism: Group Mask Summary

Each SIMD group's last byte (_states[group_start + 15]) serves as a group summary: it is set to 0 when the group contains only EEMPTY entries, allowing the search to terminate early at the group level:

// Check if an entire group is empty
int8_t group_mask(gbucket) const {
    return _states[gbucket + simd_bytes - 1];  // 0 = all empty
}

Lookup Algorithm

find_filled_bucket(key):
    H2 = hash_key2(main_bucket, key)
    next_bucket = main_bucket
    offset = 0
    do:
        vec = SIMD_LOAD(&_states[next_bucket])
        mask = MOVEMASK(CMPEQ(vec, H2))
        for each match: if key match: return bucket
        if group_mask(next_bucket) == EEMPTY: return NOT_FOUND  // group-level early exit
        if offset >= _max_probe_length: return NOT_FOUND         // global PSL limit
        next_bucket = get_next_bucket(next_bucket, ++offset)
    while true

Probing Strategy

get_next_bucket(next_bucket, offset):
    #if EMH_PSL_LINEAR
        next_bucket += 3 * simd_bytes
        if next_bucket >= _num_buckets:
            next_bucket += simd_bytes          // skip past wrap
    #else
        if offset < 7:
            next_bucket += simd_bytes * offset // quadratic-like
        else:
            next_bucket += _num_buckets/8 + simd_bytes  // jump
    #endif
    return next_bucket & _mask

Erase Strategy

Uses the same group-aware erase as emihmap1, checking if the group still has entries:

_erase(bucket):
    _pairs[bucket].~PairT()
    gbucket = bucket / simd_bytes * simd_bytes
    _states[bucket] = (group_mask(gbucket) == EEMPTY) ? EEMPTY : EDELETE

Performance Characteristics

OperationAverageWorst Case
LookupO(1) ~1-2 group probesO(n) pathological hash
InsertO(1) amortizedO(n) + rehash
EraseO(1)O(n)
IterationO(n/filled_ratio) with SIMDO(n)
Space overhead1 byte/bucket (states)~6.25% for pairs

Key Optimizations

  • Minimal metadata: Single _max_probe_length variable, no per-group offset array
  • Group-level early exit: group_mask summary byte enables fast empty-group detection
  • Adaptive probing: Hybrid quadratic + jump probing for better cache behavior
  • SIMD iterator: Same filled_mask() optimization as emihmap1

emilib Comparison Matrix

Featureemihmap1emihmap2emihmap3
Namespaceemilibemilib2emilib3
Probe trackingInline (group trailer byte)Separate _offset[] arrayGlobal _max_probe_length
SIMD group utilization15/16 slots (93.75%)16/16 slots (100%)15/16 slots (93.75%)
Early terminationPer-group probe depthPer-group offset + EEMPTY checkGroup mask + global PSL
Erase optimizationGroup-aware EEMPTY/EDELETEBackward deletion (optional)Group-aware EEMPTY/EDELETE
Load factorFixed 5/6 ≈ 0.833Configurable 0.25..0.999Configurable 0.25..0.999
Extra memoryNone_offset[] (~0.125 bytes/bucket)None
Probing strategyQuadratic + jumpLinear / quadratic / jumpQuadratic + jump
SIMD widthSSE2 / AVX2SSE2 / AVX2 / AVX-512SSE2 / AVX2 / AVX-512
Metadata per bucket1 byte1 byte1 byte

emilib vs emhash: Design Philosophy

Aspectemhash (5/6/7/8)emilib (1/2/3)
Collision resolutionLinked-bucket chainsSwiss-table-style byte probing
MetadataEmbedded in bucket structSeparate _states[] byte array
SIMD usageLimited (CTZ/bitmask)Pervasive (H2 filtering, iteration)
Key comparisonAlways requiredFiltered by H2 tag (~94% skipped)
Memory overhead1 pointer/bucket (linkage)1 byte/bucket (metadata)
Cache behaviorChasing bucket linksLinear scan of _states[]
Best forGeneral purpose, high LFSIMD-friendly integer keys, read-heavy

Selection Guide for emilib

  • emihmap1 — Best for fixed workloads with stable load factors; simplest data structure with zero extra overhead
  • emihmap2 — Best for variable workloads needing high load factors (up to 0.999); most sophisticated erase optimization; full SIMD group utilization
  • emihmap3 — Best balanced option; minimal metadata with group-level early exit; good default choice for most SIMD-accelerated use cases

Usage

#include "emilib/emihmap1.hpp"  // namespace emilib
#include "emilib/emihmap2.hpp"  // namespace emilib2
#include "emilib/emihmap3.hpp"  // namespace emilib3

emilib::HashMap<int, std::string> map1;
emilib2::HashMap<int, std::string> map2;
emilib3::HashMap<int, std::string> map3;