return n
}
+// Return a key from a group containing no empty slots, or nil if there are no
+// full groups.
+//
+// Also returns nil if a group is full but contains entirely deleted slots.
+func (m *Map) KeyFromFullGroup() unsafe.Pointer {
+ var lastTab *table
+ for _, t := range m.directory {
+ if t == lastTab {
+ continue
+ }
+ lastTab = t
+
+ for i := uint64(0); i <= t.groups.lengthMask; i++ {
+ g := t.groups.group(i)
+ match := g.ctrls().matchEmpty()
+ if match != 0 {
+ continue
+ }
+
+ // All full or deleted slots.
+ for j := uint32(0); j < abi.SwissMapGroupSlots; j++ {
+ if g.ctrls().get(j) == ctrlDeleted {
+ continue
+ }
+ return g.key(j)
+ }
+ }
+ }
+
+ return nil
+}
+
func (m *Map) TableFor(key unsafe.Pointer) *table {
hash := m.typ.Hasher(key, m.seed)
idx := m.directoryIndex(hash)
return m.directory[idx]
}
+func (t *table) GrowthLeft() uint64 {
+ return uint64(t.growthLeft)
+}
+
// Returns the start address of the groups array.
func (t *table) GroupsStart() unsafe.Pointer {
return t.groups.data
// slot as empty, as there could be more slots used later in a probe sequence
// and this deletion would cause probing to stop too early. Instead, we mark
// such slots as "deleted" with a tombstone. If the group still has an empty
-// slot, we don't need a tombstone and directly mark the slot empty. Currently,
-// tombstone are only cleared during grow, as an in-place cleanup complicates
-// iteration.
+// slot, we don't need a tombstone and directly mark the slot empty. Insert
+// prioritizes reuse of tombstones over filling an empty slots. Otherwise,
+// tombstones are only completely cleared during grow, as an in-place cleanup
+// complicates iteration.
//
// Growth
//
}
}
+// Put should reuse a deleted slot rather than consuming an empty slot.
+func TestTablePutDelete(t *testing.T) {
+ // Put will reuse the first deleted slot it encounters.
+ //
+ // This is awkward to test because Delete will only install ctrlDeleted
+ // if the group is full, otherwise it goes straight to empty.
+ //
+ // So first we must add to the table continuously until we happen to
+ // fill a group.
+
+ m, _ := maps.NewTestMap[uint32, uint32](8)
+
+ key := uint32(0)
+ elem := uint32(256 + 0)
+
+ for {
+ key += 1
+ elem += 1
+
+ m.Put(unsafe.Pointer(&key), unsafe.Pointer(&elem))
+
+ // Normally a Put that fills a group would fill it with the
+ // inserted key, so why search the whole map for a potentially
+ // different key in a full group?
+ //
+ // Put may grow/split a table. Initial construction of the new
+ // table(s) could result in a full group consisting of
+ // arbitrary keys.
+ fullKeyPtr := m.KeyFromFullGroup()
+ if fullKeyPtr != nil {
+ // Found a full group.
+ key = *(*uint32)(fullKeyPtr)
+ elem = 256 + key
+ break
+ }
+ }
+
+ // Key is in a full group. Deleting it will result in a ctrlDeleted
+ // slot.
+ m.Delete(unsafe.Pointer(&key))
+
+ // Re-insert key. This should reuse the deleted slot rather than
+ // consuming space.
+ tabWant := m.TableFor(unsafe.Pointer(&key))
+ growthLeftWant := tabWant.GrowthLeft()
+
+ m.Put(unsafe.Pointer(&key), unsafe.Pointer(&elem))
+
+ tabGot := m.TableFor(unsafe.Pointer(&key))
+ growthLeftGot := tabGot.GrowthLeft()
+
+ if tabGot != tabWant {
+ // There shouldn't be a grow, as replacing a deleted slot
+ // doesn't require more space.
+ t.Errorf("Put(%d) grew table got %v want %v map %v", key, tabGot, tabWant, m)
+ }
+
+ if growthLeftGot != growthLeftWant {
+ t.Errorf("GrowthLeft got %d want %d: map %v tab %v", growthLeftGot, growthLeftWant, m, tabGot)
+ }
+}
+
func TestTableIteration(t *testing.T) {
m, _ := maps.NewTestMap[uint32, uint64](8)
// 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.
func (t *table) PutSlot(m *Map, hash uintptr, key unsafe.Pointer) (unsafe.Pointer, bool) {
seq := makeProbeSeq(h1(hash), t.groups.lengthMask)
+ // As we look for a match, keep track of the first deleted slot we
+ // find, which we'll use to insert the new entry if necessary.
+ var firstDeletedGroup groupReference
+ var firstDeletedSlot uint32
+
for ; ; seq = seq.next() {
g := t.groups.group(seq.offset)
match := g.ctrls().matchH2(h2(hash))
match = match.removeFirst()
}
+ // No existing slot for this key in this group. Is this the end
+ // of the probe sequence?
match = g.ctrls().matchEmpty()
if match != 0 {
// Finding an empty slot means we've reached the end of
// the probe sequence.
+ var i uint32
+
+ // If we found a deleted slot along the way, we can
+ // replace it without consuming growthLeft.
+ if firstDeletedGroup.data != nil {
+ g = firstDeletedGroup
+ i = firstDeletedSlot
+ t.growthLeft++ // will be decremented below to become a no-op.
+ } else {
+ // Otherwise, use the empty slot.
+ i = match.first()
+ }
+
// 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)
return slotElem, true
}
- // 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(m)
return nil, false
}
+
+ // No empty slots in this group. Check for a deleted
+ // slot, which we'll use if we don't find a match later
+ // in the probe sequence.
+ //
+ // We only need to remember a single deleted slot.
+ if firstDeletedGroup.data == nil {
+ // Since we already checked for empty slots
+ // above, matches here must be deleted slots.
+ match = g.ctrls().matchEmptyOrDeleted()
+ if match != 0 {
+ firstDeletedGroup = g
+ firstDeletedSlot = match.first()
+ }
+ }
}
}