Skip to content

perf(iterator): lazy-allocate Item.slice#2292

Open
shaunpatterson wants to merge 1 commit into
dgraph-io:mainfrom
shaunpatterson:perf/item-slice-lazy
Open

perf(iterator): lazy-allocate Item.slice#2292
shaunpatterson wants to merge 1 commit into
dgraph-io:mainfrom
shaunpatterson:perf/item-slice-lazy

Conversation

@shaunpatterson

Copy link
Copy Markdown

Summary

newItem eagerly allocated a y.Slice wrapper per Item even though both users of item.slice (yieldItemValue and prefetchValue) already nil-check and lazy-init it before any access. Iterators that never read values (KeyOnly, or AllVersions=true with PrefetchValues=false — e.g. dgraph's posting list rollup walking ~8 versions per cache miss) were paying a 24-byte allocation per Item creation for a struct they never touched.

Drop the eager new(y.Slice) and rely on the existing lazy-init. Add a regression test covering all three value-reading paths (PrefetchValues=true, synchronous Item.Value, ValueCopy) to ensure the lazy initialization stays correct.

Benchmark — BenchmarkRollupKeyIterator (Apple M4 Max, 5×3s)

ns/op B/op allocs/op
before ~910 873 18
after ~855 825 16

~6% faster, 48 B/op saved, 2 fewer allocations per iterator setup.

Test plan

  • go test ./... passes
  • New TestRegressionLazyItemSliceValueRead covers all three value-read paths

🤖 Generated with Claude Code

@shaunpatterson shaunpatterson requested a review from a team as a code owner May 26, 2026 00:44
Replaces the lazy-init wrapper (a y.Slice field with nil-check + lazy
allocation) with the more direct approach: the field is gone entirely
and the two former Resize call sites use make([]byte, N) instead.

The motivating requirement was 'iterators that never call Value/ValueCopy
should not pay for a buffer wrapper they will never touch' (dgraph's
posting list rollup). With the field deleted, that requirement falls
out for free: the wrapper never exists at all.

Also drops the dead *y.Slice parameter from vlog.Read (it was named _
inside the function — the only caller passed item.slice solely to satisfy
the signature). Removes a misleading hint that the slice is 'used by
vlog.Read'.

iterator.go:
  - Item struct: drop slice *y.Slice field
  - yieldItemValue: drop lazy-init; use make([]byte, N) inline; call
    vlog.Read without the dead param
  - prefetchValue: use make([]byte, N) instead of item.slice.Resize
  - newItem: drop the now-unneeded slice initialization

value.go:
  - vlog.Read signature: drop the unused *y.Slice parameter

iterator_lazy_slice_test.go:
  - Regression test (unchanged): exercises Value/ValueCopy on items
    produced by the new path to ensure no nil-deref.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant