From e30f9271bf65026f64b594e3b6d3f123d454ebd1 Mon Sep 17 00:00:00 2001 From: Zac Krebbekx Date: Wed, 27 May 2026 15:16:02 +0930 Subject: [PATCH] Allocate batchRequest only on cache miss Loader.Load allocated the batchRequest and its done channel at the top of the function, before the cache lookup. req is only ever used on a cache miss (it is sent to the batcher), so on a cache hit it was allocated and immediately discarded. Move the allocation below the cache-hit early return so cached loads don't pay for it. Benchmark (Load on a primed key, -benchmem): before: 3 allocs/op 160 B/op 45.7 ns/op after: 1 alloc/op 16 B/op 20.5 ns/op Co-Authored-By: Claude Opus 4.7 --- dataloader.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dataloader.go b/dataloader.go index c4ea540..d117c22 100644 --- a/dataloader.go +++ b/dataloader.go @@ -227,10 +227,6 @@ func NewBatchedLoader[K comparable, V any](batchFn BatchFunc[K, V], opts ...Opti // the registered BatchFunc. func (l *Loader[K, V]) Load(originalContext context.Context, key K) Thunk[V] { ctx, finish := l.tracer.TraceLoad(originalContext, key) - req := &batchRequest[K, V]{ - key: key, - done: make(chan struct{}), - } // We need to lock both the batchLock and cacheLock because the batcher can // reset the cache when either the batchCap or the wait time is reached. @@ -254,6 +250,13 @@ func (l *Loader[K, V]) Load(originalContext context.Context, key K) Thunk[V] { return v } + // Only a cache miss needs a batch request: allocating it (and its channel) + // before the cache check above wastes an allocation on every cache hit. + req := &batchRequest[K, V]{ + key: key, + done: make(chan struct{}), + } + thunk := func() (V, error) { <-req.done result := req.result.Load()