From ec0406db44054aa51a0a0f103ad8aa27dacf2cda Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Sun, 24 May 2026 16:27:58 -0400 Subject: [PATCH] perf(db): eliminate closure allocation in getMemTables cleanup getMemTables previously returned (tables, cleanupFunc) where cleanupFunc was a closure capturing the tables slice. That closure escaped to the heap on every call (one extra allocation per iterator construction in the rollup path). Split into: func (db *DB) getMemTables() []*memTable func decrMemTables([]*memTable) Callers do `defer decrMemTables(tables)` after the getMemTables call. The slice is fully populated before the defer evaluates its argument, so the captured slice header points to the correct backing array; no behavioral change. decrMemTables is a free function (no receiver) so the deferred call record doesn't pay for a *DB pointer alongside the slice header. Saves 1 alloc and 32 B per iterator construction on BenchmarkRollupKeyIterator: before: 801 B/op, 14 allocs/op after: 769 B/op, 13 allocs/op Co-Authored-By: Claude Opus 4.7 --- db.go | 28 ++++++++++++++++++++-------- iterator.go | 4 ++-- stream_writer.go | 4 ++-- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/db.go b/db.go index 8a1021308..8012d3922 100644 --- a/db.go +++ b/db.go @@ -710,8 +710,14 @@ func (db *DB) Sync() error { return y.CombineErrors(memtableSyncError, vLogSyncError) } -// getMemtables returns the current memtables and get references. -func (db *DB) getMemTables() ([]*memTable, func()) { +// getMemTables returns the active set of memtables (mutable + immutables) +// with their refcounts already incremented. Callers MUST release the +// references by calling decrMemTables(tables) — typically via defer. +// +// The two-method shape (instead of returning a cleanup closure) avoids the +// per-call closure allocation that the previous API forced on every iterator +// construction and stream-writer setup. +func (db *DB) getMemTables() []*memTable { db.lock.RLock() defer db.lock.RUnlock() @@ -730,10 +736,16 @@ func (db *DB) getMemTables() ([]*memTable, func()) { tables = append(tables, db.imm[last-i]) db.imm[last-i].IncrRef() } - return tables, func() { - for _, tbl := range tables { - tbl.DecrRef() - } + return tables +} + +// decrMemTables releases the refcounts taken by getMemTables. Pair with +// `defer decrMemTables(tables)` after a getMemTables call. Free function +// (no receiver) so the deferred call record doesn't have to capture a +// *DB pointer alongside the slice header. +func decrMemTables(tables []*memTable) { + for _, tbl := range tables { + tbl.DecrRef() } } @@ -755,8 +767,8 @@ func (db *DB) get(key []byte) (y.ValueStruct, error) { if db.IsClosed() { return y.ValueStruct{}, ErrDBClosed } - tables, decr := db.getMemTables() // Lock should be released. - defer decr() + tables := db.getMemTables() // Lock should be released. + defer decrMemTables(tables) var maxVs y.ValueStruct version := y.ParseTs(key) diff --git a/iterator.go b/iterator.go index f57cfa4c9..e911c51fb 100644 --- a/iterator.go +++ b/iterator.go @@ -470,8 +470,8 @@ func (txn *Txn) NewIterator(opt IteratorOptions) *Iterator { txn.numIterators.Add(1) // TODO: If Prefix is set, only pick those memtables which have keys with the prefix. - tables, decr := txn.db.getMemTables() - defer decr() + tables := txn.db.getMemTables() + defer decrMemTables(tables) txn.db.vlog.incrIteratorCount() var iters []y.Iterator if itr := txn.newPendingWritesIterator(opt.Reverse); itr != nil { diff --git a/stream_writer.go b/stream_writer.go index a3d563f1a..e49472ac4 100644 --- a/stream_writer.go +++ b/stream_writer.go @@ -93,8 +93,8 @@ func (sw *StreamWriter) PrepareIncremental() error { } sw.done = func() { once.Do(done) } - mts, decr := sw.db.getMemTables() - defer decr() + mts := sw.db.getMemTables() + defer decrMemTables(mts) for _, m := range mts { if !m.sl.Empty() { return fmt.Errorf("Unable to do incremental writes because MemTable has data")