Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

map: avoid allocations in MapIterator.Next #1243

Merged
merged 1 commit into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -1411,8 +1411,10 @@ func marshalMap(m *Map, length int) ([]byte, error) {
//
// See Map.Iterate.
type MapIterator struct {
target *Map
curKey []byte
target *Map
// Temporary storage to avoid allocations in Next(). This is any instead
// of []byte to avoid allocations.
cursor any
count, maxEntries uint32
done bool
err error
Expand Down Expand Up @@ -1440,34 +1442,30 @@ func (mi *MapIterator) Next(keyOut, valueOut interface{}) bool {
return false
}

// For array-like maps NextKeyBytes returns nil only on after maxEntries
// For array-like maps NextKey returns nil only after maxEntries
// iterations.
for mi.count <= mi.maxEntries {
var nextKey []byte
if mi.curKey == nil {
// Pass nil interface to NextKeyBytes to make sure the Map's first key
if mi.cursor == nil {
// Pass nil interface to NextKey to make sure the Map's first key
// is returned. If we pass an uninitialized []byte instead, it'll see a
// non-nil interface and try to marshal it.
nextKey, mi.err = mi.target.NextKeyBytes(nil)

mi.curKey = make([]byte, mi.target.keySize)
mi.cursor = make([]byte, mi.target.keySize)
mi.err = mi.target.NextKey(nil, mi.cursor)
} else {
nextKey, mi.err = mi.target.NextKeyBytes(mi.curKey)
}
if mi.err != nil {
mi.err = fmt.Errorf("get next key: %w", mi.err)
return false
mi.err = mi.target.NextKey(mi.cursor, mi.cursor)
}

if nextKey == nil {
if errors.Is(mi.err, ErrKeyNotExist) {
mi.done = true
mi.err = nil
return false
} else if mi.err != nil {
mi.err = fmt.Errorf("get next key: %w", mi.err)
return false
}

mi.curKey = nextKey

mi.count++
mi.err = mi.target.Lookup(nextKey, valueOut)
mi.err = mi.target.Lookup(mi.cursor, valueOut)
if errors.Is(mi.err, ErrKeyNotExist) {
// Even though the key should be valid, we couldn't look up
// its value. If we're iterating a hash map this is probably
Expand All @@ -1484,10 +1482,11 @@ func (mi *MapIterator) Next(keyOut, valueOut interface{}) bool {
return false
}

buf := mi.cursor.([]byte)
if ptr, ok := keyOut.(unsafe.Pointer); ok {
copy(unsafe.Slice((*byte)(ptr), len(nextKey)), nextKey)
copy(unsafe.Slice((*byte)(ptr), len(buf)), buf)
} else {
mi.err = sysenc.Unmarshal(keyOut, nextKey)
mi.err = sysenc.Unmarshal(keyOut, buf)
}

return mi.err == nil
Expand Down
26 changes: 26 additions & 0 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,32 @@ func TestMapIterate(t *testing.T) {
}
}

func TestMapIteratorAllocations(t *testing.T) {
arr, err := NewMap(&MapSpec{
Type: Array,
KeySize: 4,
ValueSize: 4,
MaxEntries: 10,
})
if err != nil {
t.Fatal(err)
}
defer arr.Close()

var k, v uint32
iter := arr.Iterate()
// Allocate any necessary temporary buffers.
qt.Assert(t, iter.Next(&k, &v), qt.IsTrue)

allocs := testing.AllocsPerRun(1, func() {
if !iter.Next(&k, &v) {
t.Fatal("Next failed")
}
})

qt.Assert(t, allocs, qt.Equals, float64(0))
}

func TestMapIterateHashKeyOneByteFull(t *testing.T) {
hash, err := NewMap(&MapSpec{
Type: Hash,
Expand Down