Experiments
0

Configure Feed

Select the types of activity you want to include in your feed.

README.md

hashtriemap#

A generic concurrent hash trie map for Go, extracted and extended from internal/sync.HashTrieMap in the Go standard library.

go get dario.cat/x/hashtriemap

Requires Go 1.23 or later (uses range-over-func iterators).


Overview#

HashTrieMap[K, V comparable] is a concurrent map with the same surface API as sync.Map but with full generics, lock-free reads, and a producer/consumer pattern that batches writes per goroutine.

The trie is a 16-way (4 bits of hash per level) tree of indirect (branch) nodes with entry (leaf) nodes at the tips. Keys are routed by maphash.Comparable. True hash collisions are chained via an overflow linked list on the leaf. The trie structure is never shrunk during normal operation — deletions set an atomic tombstone and Compact prunes lazily at cycle boundaries.


Quick start#

var m hashtriemap.HashTrieMap[string, int]

m.Store("hits", 1)

v, ok := m.Load("hits")   // v=1, ok=true
m.Delete("hits")

The zero value is ready to use. The map must not be copied after first use.


API#

Reads#

Method Description
Load(key) (V, bool) Lock-free. Returns the value and whether the key exists.
LoadOrStore(key, value) (V, bool) Returns the existing value if present; otherwise stores and returns the new value.

Writes#

Method Description
Store(key, value) Store a value. Overwrites existing entries in-place (atomic pointer swap, no allocation).
Swap(key, new) (V, bool) Store and return the previous value. Fast path is lock-free for existing keys.
CompareAndSwap(key, old, new) bool Swap only if the current value equals old.

Deletes#

Deletes mark entries as tombstoned (atomic flag). The entry node stays in the trie until Compact is called. Load, Range, and All skip tombstoned entries transparently.

Method Description
Delete(key) Mark key as deleted. No-op if absent.
LoadAndDelete(key) (V, bool) Delete and return the previous value.
CompareAndDelete(key, old) bool Delete only if the current value equals old.

Iteration#

Method Description
Range(func(K, V) bool) Call f for each live entry. Stop early by returning false.
All() func(yield func(K, V) bool) Returns a range iterator for use in for k, v := range m.All().
Snapshot() map[K]V Copy all live entries into a plain map[K]V. Use only when random access is needed; prefer Range/All for pure iteration.

Lifecycle#

Method Description
Clear() Replace the root, effectively emptying the map.
Compact() Prune tombstoned entries and collapse empty branch nodes. Call at cycle boundaries (see below).

Producer pattern#

Producer is a per-goroutine write buffer. Multiple writes to the same key within a single cycle are coalesced — only the last value is flushed to the shared map. This reduces hash computations, trie traversals, mutex acquisitions, and allocations proportional to the key reuse rate.

var m hashtriemap.HashTrieMap[string, int]

var wg sync.WaitGroup
for g := range numProducers {
    wg.Add(1)
    go func() {
        defer wg.Done()
        p := m.NewProducer()
        defer p.Flush()           // commit buffered writes on exit
        for _, event := range work {
            p.Store(event.Key, event.Value)
        }
    }()
}
wg.Wait()

// Producers are done — trie is stable. Iterate with no copy.
m.Range(func(k string, v int) bool {
    process(k, v)
    return true
})

// Prune tombstones before the next cycle.
m.Compact()

Producer.Delete tombstones the key in the shared map immediately. A subsequent Store to the same key in the same producer will re-create the entry on Flush.


Implementation notes#

In-place value updateentry.value is an atomic.Pointer[V]. Overwriting an existing key swaps the pointer without allocating a new node or acquiring the parent mutex.

Tombstone deletionDelete/LoadAndDelete set an atomic.Bool on the entry. The trie structure is unchanged; Compact prunes lazily.

Per-node mutexes — structural changes (inserting a new key, expanding a leaf into a subtree) acquire only the parent indirect node's mutex. Reads and in-place updates are lock-free.

Pointer tagging — the child type (entry vs. indirect) is encoded in the low bit of the stored pointer rather than in a separate header struct, saving 8 bytes per entry.

sync.Pool — entry and indirect nodes are pooled per generic instantiation. Compact returns pruned nodes to the pool.


When to use this#

Scenario Recommendation
Read-heavy, stable key set sync.Map or this map both work; this map has full generics.
Cycle-based producer/consumer (batch writes, then scan) This map — use Producer + Compact.
Frequent deletes with bounded memory Call Compact between cycles; otherwise tombstones accumulate.
Low-cardinality key set with high write contention map + sync.RWMutex is simpler and often faster.
Need ordered iteration Neither this map nor sync.Map; use a sorted structure.