Skip to content

perf: hoist newValue allocations out of typed map decode loop (#65)#69

Merged
xe-nvdk merged 1 commit into
v6from
perf/typed-map-newvalue-hoist
Apr 15, 2026
Merged

perf: hoist newValue allocations out of typed map decode loop (#65)#69
xe-nvdk merged 1 commit into
v6from
perf/typed-map-newvalue-hoist

Conversation

@xe-nvdk
Copy link
Copy Markdown
Member

@xe-nvdk xe-nvdk commented Apr 15, 2026

Summary

Closes #65. decodeTypedMapValue called d.newValue() per map entry — 2N reflect.New() allocations per N-entry typed map. Hoist the key/value reflect.Value slots outside the loop and reuse them, zero-resetting before each iteration.

Zeroing is a correctness requirement, not cosmetic. decodeSliceValue reuses an existing slice's backing array when v.Cap() >= n; without zeroing the hoisted slot, iteration 2 of a map[K][]V decode would clobber iteration 1's already-stored slice. SetMapIndex copies the slice header but the backing array is shared.

Measured impact

BenchmarkLargeMapIntInt (1000-entry map[int]int):

metric before after delta
allocs/op 4000 2002 -50%
B/op 32006 16021 -50%
ns/op ~140us ~124us -10%

Test plan

  • go test ./... passes
  • go test -race ./... passes
  • go vet ./... clean
  • Two regression tests verified load-bearing (both fail without the per-iteration Set(zero) lines):
    • TestDecodeTypedMapSliceValuesAreIndependentmap[int][]int backing-array aliasing
    • TestDecodeTypedMapStructValuesAreIndependent — struct with []string field aliasing
  • types_test.go typeTest additions: map[int][]int, map[string][]string, map[string]map[string]int
  • New BenchmarkLargeMapIntInt in bench_test.go for future perf regression detection

Closes #65. decodeTypedMapValue called d.newValue() per map entry,
producing 2N reflect.New() allocations per N-entry typed map. Hoist the
key and value reflect.Value slots outside the loop and reuse them across
iterations, zero-resetting before each use. Takes typed-map decode from
2N reflect.New() calls to 2 per map.

The zeroing is a correctness requirement, not cosmetic: decodeSliceValue
reuses an existing slice's backing array when v.Cap() >= n, so without
zeroing between iterations, iteration 2 would clobber the slice
iteration 1 already stored in the map (SetMapIndex copies the slice
header but the backing array is shared). Two regression tests cover
this: map[int][]int and map[int]struct{A int; B []string} — both fail
without the per-iteration Set(zero) lines.

BenchmarkLargeMapIntInt (1000-entry map[int]int):
  before: 4000 allocs/op, 32006 B/op, ~140us/op
  after:  2002 allocs/op, 16021 B/op, ~124us/op
  (-50% allocs, -50% bytes, -10% ns)
@xe-nvdk xe-nvdk merged commit ed17032 into v6 Apr 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: reduce newValue() allocations in typed map decode loop

1 participant