aboutsummaryrefslogtreecommitdiff
path: root/src/internal/runtime/maps/table.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/internal/runtime/maps/table.go')
-rw-r--r--src/internal/runtime/maps/table.go669
1 files changed, 669 insertions, 0 deletions
diff --git a/src/internal/runtime/maps/table.go b/src/internal/runtime/maps/table.go
new file mode 100644
index 0000000000..3516b92fba
--- /dev/null
+++ b/src/internal/runtime/maps/table.go
@@ -0,0 +1,669 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package maps implements Go's builtin map type.
+package maps
+
+import (
+ "internal/runtime/maps/internal/abi"
+ "unsafe"
+)
+
+// table is a Swiss table hash table structure.
+//
+// Each table is a complete hash table implementation.
+type table struct {
+ // The number of filled slots (i.e. the number of elements in the table).
+ used uint64
+
+ // TODO(prattmic): Old maps pass this into every call instead of
+ // keeping a reference in the map header. This is probably more
+ // efficient and arguably more robust (crafty users can't reach into to
+ // the map to change its type), but I leave it here for now for
+ // simplicity.
+ typ *abi.SwissMapType
+
+ // seed is the hash seed, computed as a unique random number per table.
+ // TODO(prattmic): Populate this on table initialization.
+ seed uintptr
+
+ // groups is an array of slot groups. Each group holds abi.SwissMapGroupSlots
+ // key/elem slots and their control bytes.
+ //
+ // TODO(prattmic): keys and elements are interleaved to maximize
+ // locality, but it comes at the expense of wasted space for some types
+ // (consider uint8 key, uint64 element). Consider placing all keys
+ // together in these cases to save space.
+ //
+ // TODO(prattmic): Support indirect keys/values? This means storing
+ // keys/values as pointers rather than inline in the slot. This avoid
+ // bloating the table size if either type is very large.
+ groups groupsReference
+
+ // The total number of slots (always 2^N). Equal to
+ // `(groups.lengthMask+1)*abi.SwissMapGroupSlots`.
+ capacity uint64
+
+ // The number of slots we can still fill without needing to rehash.
+ //
+ // We rehash when used + tombstones > loadFactor*capacity, including
+ // tombstones so the table doesn't overfill with tombstones. This field
+ // counts down remaining empty slots before the next rehash.
+ growthLeft uint64
+
+ // clearSeq is a sequence counter of calls to Clear. It is used to
+ // detect map clears during iteration.
+ clearSeq uint64
+}
+
+func NewTable(mt *abi.SwissMapType, capacity uint64) *table {
+ return newTable(mt, capacity)
+}
+
+func newTable(mt *abi.SwissMapType, capacity uint64) *table {
+ if capacity < abi.SwissMapGroupSlots {
+ // TODO: temporary until we have a real map type.
+ capacity = abi.SwissMapGroupSlots
+ }
+
+ t := &table{
+ typ: mt,
+ }
+
+ // N.B. group count must be a power of two for probeSeq to visit every
+ // group.
+ capacity, overflow := alignUpPow2(capacity)
+ if overflow {
+ panic("rounded-up capacity overflows uint64")
+ }
+
+ t.reset(capacity)
+
+ return t
+}
+
+// reset resets the table with new, empty groups with the specified new total
+// capacity.
+func (t *table) reset(capacity uint64) {
+ ac, overflow := alignUpPow2(capacity)
+ if capacity != ac || overflow {
+ panic("capacity must be a power of two")
+ }
+
+ groupCount := capacity / abi.SwissMapGroupSlots
+ t.groups = newGroups(t.typ, groupCount)
+ t.capacity = capacity
+ t.resetGrowthLeft()
+
+ for i := uint64(0); i <= t.groups.lengthMask; i++ {
+ g := t.groups.group(i)
+ g.ctrls().setEmpty()
+ }
+}
+
+// Preconditions: table must be empty.
+func (t *table) resetGrowthLeft() {
+ var growthLeft uint64
+ if t.capacity == 0 {
+ // No real reason to support zero capacity table, since an
+ // empty Map simply won't have a table.
+ panic("table must have positive capacity")
+ } else if t.capacity <= abi.SwissMapGroupSlots {
+ // If the map fits in a single group then we're able to fill all of
+ // the slots except 1 (an empty slot is needed to terminate find
+ // operations).
+ //
+ // TODO(go.dev/issue/54766): With a special case in probing for
+ // single-group tables, we could fill all slots.
+ growthLeft = t.capacity - 1
+ } else {
+ if t.capacity*maxAvgGroupLoad < t.capacity {
+ // TODO(prattmic): Do something cleaner.
+ panic("overflow")
+ }
+ growthLeft = (t.capacity * maxAvgGroupLoad) / abi.SwissMapGroupSlots
+ }
+ t.growthLeft = growthLeft
+}
+
+func (t *table) Used() uint64 {
+ return t.used
+}
+
+// Get performs a lookup of the key that key points to. It returns a pointer to
+// the element, or false if the key doesn't exist.
+func (t *table) Get(key unsafe.Pointer) (unsafe.Pointer, bool) {
+ _, elem, ok := t.getWithKey(key)
+ return elem, ok
+}
+
+// getWithKey performs a lookup of key, returning a pointer to the version of
+// the key in the map in addition to the element.
+//
+// This is relevant when multiple different key values compare equal (e.g.,
+// +0.0 and -0.0). When a grow occurs during iteration, iteration perform a
+// lookup of keys from the old group in the new group in order to correctly
+// expose updated elements. For NeedsKeyUpdate keys, iteration also must return
+// the new key value, not the old key value.
+func (t *table) getWithKey(key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer, bool) {
+ // TODO(prattmic): We could avoid hashing in a variety of special
+ // cases.
+ //
+ // - One group maps with simple keys could iterate over all keys and
+ // compare them directly.
+ // - One entry maps could just directly compare the single entry
+ // without hashing.
+ // - String keys could do quick checks of a few bytes before hashing.
+ hash := t.typ.Hasher(key, t.seed)
+
+ // To find the location of a key in the table, we compute hash(key). From
+ // h1(hash(key)) and the capacity, we construct a probeSeq that visits
+ // every group of slots in some interesting order. See [probeSeq].
+ //
+ // We walk through these indices. At each index, we select the entire
+ // group starting with that index and extract potential candidates:
+ // occupied slots with a control byte equal to h2(hash(key)). The key
+ // at candidate slot i is compared with key; if key == g.slot(i).key
+ // we are done and return the slot; if there is an empty slot in the
+ // group, we stop and return an error; otherwise we continue to the
+ // next probe index. Tombstones (ctrlDeleted) effectively behave like
+ // full slots that never match the value we're looking for.
+ //
+ // The h2 bits ensure when we compare a key we are likely to have
+ // actually found the object. That is, the chance is low that keys
+ // compare false. Thus, when we search for an object, we are unlikely
+ // to call Equal many times. This likelihood can be analyzed as follows
+ // (assuming that h2 is a random enough hash function).
+ //
+ // Let's assume that there are k "wrong" objects that must be examined
+ // in a probe sequence. For example, when doing a find on an object
+ // that is in the table, k is the number of objects between the start
+ // of the probe sequence and the final found object (not including the
+ // final found object). The expected number of objects with an h2 match
+ // is then k/128. Measurements and analysis indicate that even at high
+ // load factors, k is less than 32, meaning that the number of false
+ // positive comparisons we must perform is less than 1/8 per find.
+ seq := makeProbeSeq(h1(hash), t.groups.lengthMask)
+ for ; ; seq = seq.next() {
+ g := t.groups.group(seq.offset)
+
+ match := g.ctrls().matchH2(h2(hash))
+
+ for match != 0 {
+ i := match.first()
+
+ slotKey := g.key(i)
+ if t.typ.Key.Equal(key, slotKey) {
+ return slotKey, g.elem(i), true
+ }
+ match = match.removeFirst()
+ }
+
+ match = g.ctrls().matchEmpty()
+ if match != 0 {
+ // Finding an empty slot means we've reached the end of
+ // the probe sequence.
+ return nil, nil, false
+ }
+ }
+}
+
+func (t *table) Put(key, elem unsafe.Pointer) {
+ slotElem := t.PutSlot(key)
+ typedmemmove(t.typ.Elem, slotElem, elem)
+}
+
+// PutSlot returns a pointer to the element slot where an inserted element
+// should be written.
+//
+// PutSlot never returns nil.
+func (t *table) PutSlot(key unsafe.Pointer) unsafe.Pointer {
+ hash := t.typ.Hasher(key, t.seed)
+
+ seq := makeProbeSeq(h1(hash), t.groups.lengthMask)
+
+ for ; ; seq = seq.next() {
+ g := t.groups.group(seq.offset)
+ match := g.ctrls().matchH2(h2(hash))
+
+ // Look for an existing slot containing this key.
+ for match != 0 {
+ i := match.first()
+
+ slotKey := g.key(i)
+ if t.typ.Key.Equal(key, slotKey) {
+ if t.typ.NeedKeyUpdate() {
+ typedmemmove(t.typ.Key, slotKey, key)
+ }
+
+ slotElem := g.elem(i)
+
+ t.checkInvariants()
+ return slotElem
+ }
+ match = match.removeFirst()
+ }
+
+ match = g.ctrls().matchEmpty()
+ if match != 0 {
+ // Finding an empty slot means we've reached the end of
+ // the probe sequence.
+
+ // If there is room left to grow, just insert the new entry.
+ if t.growthLeft > 0 {
+ i := match.first()
+
+ slotKey := g.key(i)
+ typedmemmove(t.typ.Key, slotKey, key)
+ slotElem := g.elem(i)
+
+ g.ctrls().set(i, ctrl(h2(hash)))
+ t.growthLeft--
+ t.used++
+
+ t.checkInvariants()
+ return slotElem
+ }
+
+ // TODO(prattmic): While searching the probe sequence,
+ // we may have passed deleted slots which we could use
+ // for this entry.
+ //
+ // At the moment, we leave this behind for
+ // rehash to free up.
+ //
+ // cockroachlabs/swiss restarts search of the probe
+ // sequence for a deleted slot.
+ //
+ // TODO(go.dev/issue/54766): We want this optimization
+ // back. We could search for the first deleted slot
+ // during the main search, but only use it if we don't
+ // find an existing entry.
+
+ t.rehash()
+
+ // Note that we don't have to restart the entire Put process as we
+ // know the key doesn't exist in the map.
+ slotElem := t.uncheckedPutSlot(hash, key)
+ t.used++
+ t.checkInvariants()
+ return slotElem
+ }
+ }
+}
+
+// uncheckedPutSlot inserts an entry known not to be in the table, returning an
+// entry to the element slot where the element should be written. Used by
+// PutSlot after it has failed to find an existing entry to overwrite duration
+// insertion.
+//
+// Updates growthLeft if necessary, but does not update used.
+//
+// Requires that the entry does not exist in the table, and that the table has
+// room for another element without rehashing.
+//
+// Never returns nil.
+func (t *table) uncheckedPutSlot(hash uintptr, key unsafe.Pointer) unsafe.Pointer {
+ if t.growthLeft == 0 {
+ panic("invariant failed: growthLeft is unexpectedly 0")
+ }
+
+ // Given key and its hash hash(key), to insert it, we construct a
+ // probeSeq, and use it to find the first group with an unoccupied (empty
+ // or deleted) slot. We place the key/value into the first such slot in
+ // the group and mark it as full with key's H2.
+ seq := makeProbeSeq(h1(hash), t.groups.lengthMask)
+ for ; ; seq = seq.next() {
+ g := t.groups.group(seq.offset)
+
+ match := g.ctrls().matchEmpty()
+ if match != 0 {
+ i := match.first()
+
+ slotKey := g.key(i)
+ typedmemmove(t.typ.Key, slotKey, key)
+ slotElem := g.elem(i)
+
+ if g.ctrls().get(i) == ctrlEmpty {
+ t.growthLeft--
+ }
+ g.ctrls().set(i, ctrl(h2(hash)))
+ return slotElem
+ }
+ }
+}
+
+func (t *table) Delete(key unsafe.Pointer) {
+ hash := t.typ.Hasher(key, t.seed)
+
+ seq := makeProbeSeq(h1(hash), t.groups.lengthMask)
+ for ; ; seq = seq.next() {
+ g := t.groups.group(seq.offset)
+ match := g.ctrls().matchH2(h2(hash))
+
+ for match != 0 {
+ i := match.first()
+ slotKey := g.key(i)
+ if t.typ.Key.Equal(key, slotKey) {
+ t.used--
+
+ typedmemclr(t.typ.Key, slotKey)
+ typedmemclr(t.typ.Elem, g.elem(i))
+
+ // Only a full group can appear in the middle
+ // of a probe sequence (a group with at least
+ // one empty slot terminates probing). Once a
+ // group becomes full, it stays full until
+ // rehashing/resizing. So if the group isn't
+ // full now, we can simply remove the element.
+ // Otherwise, we create a tombstone to mark the
+ // slot as deleted.
+ if g.ctrls().matchEmpty() != 0 {
+ g.ctrls().set(i, ctrlEmpty)
+ t.growthLeft++
+ } else {
+ g.ctrls().set(i, ctrlDeleted)
+ }
+
+ t.checkInvariants()
+ return
+ }
+ match = match.removeFirst()
+ }
+
+ match = g.ctrls().matchEmpty()
+ if match != 0 {
+ // Finding an empty slot means we've reached the end of
+ // the probe sequence.
+ return
+ }
+ }
+}
+
+// tombstones returns the number of deleted (tombstone) entries in the table. A
+// tombstone is a slot that has been deleted but is still considered occupied
+// so as not to violate the probing invariant.
+func (t *table) tombstones() uint64 {
+ return (t.capacity*maxAvgGroupLoad)/abi.SwissMapGroupSlots - t.used - t.growthLeft
+}
+
+// Clear deletes all entries from the map resulting in an empty map.
+func (t *table) Clear() {
+ for i := uint64(0); i <= t.groups.lengthMask; i++ {
+ g := t.groups.group(i)
+ typedmemclr(t.typ.Group, g.data)
+ g.ctrls().setEmpty()
+ }
+
+ t.clearSeq++
+ t.used = 0
+ t.resetGrowthLeft()
+
+ // Reset the hash seed to make it more difficult for attackers to
+ // repeatedly trigger hash collisions. See issue
+ // https://github.com/golang/go/issues/25237.
+ // TODO
+ //t.seed = uintptr(rand())
+}
+
+type Iter struct {
+ key unsafe.Pointer // Must be in first position. Write nil to indicate iteration end (see cmd/compile/internal/walk/range.go).
+ elem unsafe.Pointer // Must be in second position (see cmd/compile/internal/walk/range.go).
+ typ *abi.SwissMapType
+ tab *table
+
+ // Snapshot of the groups at iteration initialization time. If the
+ // table resizes during iteration, we continue to iterate over the old
+ // groups.
+ //
+ // If the table grows we must consult the updated table to observe
+ // changes, though we continue to use the snapshot to determine order
+ // and avoid duplicating results.
+ groups groupsReference
+
+ // Copy of Table.clearSeq at iteration initialization time. Used to
+ // detect clear during iteration.
+ clearSeq uint64
+
+ // Randomize iteration order by starting iteration at a random slot
+ // offset.
+ offset uint64
+
+ // TODO: these could be merged into a single counter (and pre-offset
+ // with offset).
+ groupIdx uint64
+ slotIdx uint32
+
+ // 4 bytes of padding on 64-bit arches.
+}
+
+// Init initializes Iter for iteration.
+func (it *Iter) Init(typ *abi.SwissMapType, t *table) {
+ it.typ = typ
+ if t == nil || t.used == 0 {
+ return
+ }
+
+ it.typ = t.typ
+ it.tab = t
+ it.offset = rand()
+ it.groups = t.groups
+ it.clearSeq = t.clearSeq
+}
+
+func (it *Iter) Initialized() bool {
+ return it.typ != nil
+}
+
+// Map returns the map this iterator is iterating over.
+func (it *Iter) Map() *Map {
+ return it.tab
+}
+
+// Key returns a pointer to the current key. nil indicates end of iteration.
+//
+// Must not be called prior to Next.
+func (it *Iter) Key() unsafe.Pointer {
+ return it.key
+}
+
+// Key returns a pointer to the current element. nil indicates end of
+// iteration.
+//
+// Must not be called prior to Next.
+func (it *Iter) Elem() unsafe.Pointer {
+ return it.elem
+}
+
+// Next proceeds to the next element in iteration, which can be accessed via
+// the Key and Elem methods.
+//
+// The table can be mutated during iteration, though there is no guarantee that
+// the mutations will be visible to the iteration.
+//
+// Init must be called prior to Next.
+func (it *Iter) Next() {
+ if it.tab == nil {
+ // Map was empty at Iter.Init.
+ it.key = nil
+ it.elem = nil
+ return
+ }
+
+ // Continue iteration until we find a full slot.
+ for ; it.groupIdx <= it.groups.lengthMask; it.groupIdx++ {
+ g := it.groups.group((it.groupIdx + it.offset) & it.groups.lengthMask)
+
+ // TODO(prattmic): Skip over groups that are composed of only empty
+ // or deleted slots using matchEmptyOrDeleted() and counting the
+ // number of bits set.
+ for ; it.slotIdx < abi.SwissMapGroupSlots; it.slotIdx++ {
+ k := (it.slotIdx + uint32(it.offset)) % abi.SwissMapGroupSlots
+
+ if (g.ctrls().get(k) & ctrlEmpty) == ctrlEmpty {
+ // Empty or deleted.
+ continue
+ }
+
+ key := g.key(k)
+
+ // If groups.data has changed, then the table
+ // has grown. If the table has grown, then
+ // further mutations (changes to key->elem or
+ // deletions) will not be visible in our
+ // snapshot of groups. Instead we must consult
+ // the new groups by doing a full lookup.
+ //
+ // We still use our old snapshot of groups to
+ // decide which keys to lookup in order to
+ // avoid returning the same key twice.
+ //
+ // TODO(prattmic): Rather than growing t.groups
+ // directly, a cleaner design may be to always
+ // create a new table on grow or split, leaving
+ // behind 1 or 2 forwarding pointers. This lets
+ // us handle this update after grow problem the
+ // same way both within a single table and
+ // across split.
+ grown := it.groups.data != it.tab.groups.data
+ var elem unsafe.Pointer
+ if grown {
+ var ok bool
+ newKey, newElem, ok := it.tab.getWithKey(key)
+ if !ok {
+ // Key has likely been deleted, and
+ // should be skipped.
+ //
+ // One exception is keys that don't
+ // compare equal to themselves (e.g.,
+ // NaN). These keys cannot be looked
+ // up, so getWithKey will fail even if
+ // the key exists.
+ //
+ // However, we are in luck because such
+ // keys cannot be updated and they
+ // cannot be deleted except with clear.
+ // Thus if no clear has occurted, the
+ // key/elem must still exist exactly as
+ // in the old groups, so we can return
+ // them from there.
+ //
+ // TODO(prattmic): Consider checking
+ // clearSeq early. If a clear occurred,
+ // Next could always return
+ // immediately, as iteration doesn't
+ // need to return anything added after
+ // clear.
+ if it.clearSeq == it.tab.clearSeq && !it.tab.typ.Key.Equal(key, key) {
+ elem = g.elem(k)
+ } else {
+ continue
+ }
+ } else {
+ key = newKey
+ elem = newElem
+ }
+ } else {
+ elem = g.elem(k)
+ }
+
+ it.slotIdx++
+ if it.slotIdx >= abi.SwissMapGroupSlots {
+ it.groupIdx++
+ it.slotIdx = 0
+ }
+ it.key = key
+ it.elem = elem
+ return
+ }
+ it.slotIdx = 0
+ }
+
+ it.key = nil
+ it.elem = nil
+ return
+}
+
+func (t *table) rehash() {
+ // TODO(prattmic): SwissTables typically perform a "rehash in place"
+ // operation which recovers capacity consumed by tombstones without growing
+ // the table by reordering slots as necessary to maintain the probe
+ // invariant while eliminating all tombstones.
+ //
+ // However, it is unclear how to make rehash in place work with
+ // iteration. Since iteration simply walks through all slots in order
+ // (with random start offset), reordering the slots would break
+ // iteration.
+ //
+ // As an alternative, we could do a "resize" to new groups allocation
+ // of the same size. This would eliminate the tombstones, but using a
+ // new allocation, so the existing grow support in iteration would
+ // continue to work.
+
+ // TODO(prattmic): split table
+ // TODO(prattmic): Avoid overflow (splitting the table will achieve this)
+
+ newCapacity := 2 * t.capacity
+ t.resize(newCapacity)
+}
+
+// resize the capacity of the table by allocating a bigger array and
+// uncheckedPutting each element of the table into the new array (we know that
+// no insertion here will Put an already-present value), and discard the old
+// backing array.
+func (t *table) resize(newCapacity uint64) {
+ oldGroups := t.groups
+ oldCapacity := t.capacity
+ t.reset(newCapacity)
+
+ if oldCapacity > 0 {
+ for i := uint64(0); i <= oldGroups.lengthMask; i++ {
+ g := oldGroups.group(i)
+ for j := uint32(0); j < abi.SwissMapGroupSlots; j++ {
+ if (g.ctrls().get(j) & ctrlEmpty) == ctrlEmpty {
+ // Empty or deleted
+ continue
+ }
+ key := g.key(j)
+ elem := g.elem(j)
+ hash := t.typ.Hasher(key, t.seed)
+ slotElem := t.uncheckedPutSlot(hash, key)
+ typedmemmove(t.typ.Elem, slotElem, elem)
+ }
+ }
+ }
+
+ t.checkInvariants()
+}
+
+// probeSeq maintains the state for a probe sequence that iterates through the
+// groups in a table. The sequence is a triangular progression of the form
+//
+// p(i) := (i^2 + i)/2 + hash (mod mask+1)
+//
+// The sequence effectively outputs the indexes of *groups*. The group
+// machinery allows us to check an entire group with minimal branching.
+//
+// It turns out that this probe sequence visits every group exactly once if
+// the number of groups is a power of two, since (i^2+i)/2 is a bijection in
+// Z/(2^m). See https://en.wikipedia.org/wiki/Quadratic_probing
+type probeSeq struct {
+ mask uint64
+ offset uint64
+ index uint64
+}
+
+func makeProbeSeq(hash uintptr, mask uint64) probeSeq {
+ return probeSeq{
+ mask: mask,
+ offset: uint64(hash) & mask,
+ index: 0,
+ }
+}
+
+func (s probeSeq) next() probeSeq {
+ s.index++
+ s.offset = (s.offset + s.index) & s.mask
+ return s
+}