From b221abaf906b098d2acb310daa04a23a47aedc6c Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Sun, 24 May 2026 16:47:03 -0400 Subject: [PATCH] perf(iterator): pre-seed waste pool with embedded Item slots Iterator.prefetch() with PrefetchValues=false uses prefetchSize=2, so the first two newItem() calls on a fresh iterator always heap-allocate before the per-iterator `waste` recycling kicks in. For workloads like dgraph posting-list rollup (NewKeyIterator, AllVersions=true, PrefetchValues=false) that re-create iterators per posting list, this is 2 allocs/op of pure churn. Embed [2]Item directly in the Iterator struct and push them onto the waste pool at construction. The first two newItem() pops now return these embedded slots; only iterators that demand more items (PrefetchValues=true with PrefetchSize>2) fall back to allocating. Iterator is already only used through *Iterator and never copied by value, so the sync.WaitGroup inside Item is safe to embed. txn is assigned to each prefilled item to mirror what newItem does for heap-allocated items. BenchmarkRollupKeyIterator (Apple M4 Max): before: 829.5 ns/op 770 B/op 13 allocs/op after: ~800 ns/op 754 B/op 11 allocs/op Co-Authored-By: Claude Opus 4.7 --- iterator.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/iterator.go b/iterator.go index f57cfa4c9..e8db8758d 100644 --- a/iterator.go +++ b/iterator.go @@ -444,6 +444,20 @@ type Iterator struct { ThreadId int Alloc *z.Allocator + + // prefilledItems are seeded into the waste pool at iterator construction + // so the first two newItem() calls return them instead of heap-allocating. + // Sized to 2 because that is the maximum prefetch demand on the + // AllVersions=true, PrefetchValues=false hot path (dgraph posting-list + // rollup) where prefetch() uses prefetchSize=2 — the most common newItem + // burst pattern for short, version-walk iterators. Larger prefetch counts + // (PrefetchValues=true with PrefetchSize>2) continue to heap-allocate + // once these two are consumed. + // + // MUST be accessed only via &it.prefilledItems[i] (never copied), since + // Item contains a sync.WaitGroup. Iterator itself is always used through + // *Iterator, so the array never moves. + prefilledItems [2]Item } // NewIterator returns a new iterator. Depending upon the options, either only keys, or both @@ -487,6 +501,14 @@ func (txn *Txn) NewIterator(opt IteratorOptions) *Iterator { opt: opt, readTs: txn.readTs, } + // Seed the waste pool with the embedded items so newItem's first two + // pops return them rather than allocating fresh Items on the heap. + // txn must be set here because newItem only sets txn on freshly + // allocated items; waste-recycled items inherit it from prior use. + res.prefilledItems[0].txn = txn + res.prefilledItems[1].txn = txn + res.waste.push(&res.prefilledItems[0]) + res.waste.push(&res.prefilledItems[1]) return res }