Skip to content

perf(mapping): reduce allocations in unmarshal hot path#5606

Open
yaoyi1222 wants to merge 1 commit into
zeromicro:masterfrom
yaoyi1222:perf/mapping-hotpath-allocs
Open

perf(mapping): reduce allocations in unmarshal hot path#5606
yaoyi1222 wants to merge 1 commit into
zeromicro:masterfrom
yaoyi1222:perf/mapping-hotpath-allocs

Conversation

@yaoyi1222

@yaoyi1222 yaoyi1222 commented May 26, 2026

Copy link
Copy Markdown

Two small, behavior-preserving changes that cut allocations on the config-load and per-request request-parsing paths.

getValue

The form/path unmarshalers in rest/httpx use WithOpaqueKeys, and opaque keys are never split on the delimiter, so they always resolve to a single lookup. readKeys allocates a one-element []string{key} for each such key, which getValueWithChainedKeys then collapses to a single Valuer.Value call. Call Value directly and skip the slice allocation. This is on the per-request hot path.

join

fullName is only used to build error messages, yet join() runs a strings.Builder (and an allocating String() copy) for every named field. When at most one segment is non-empty (top-level fields, where the parent name is empty) return it directly. Error messages are byte-identical.

Benchmarks

benchstat, Apple M1 Pro, -count=8, all p<0.05:

rest/httpx  ParseAuto:        30 -> 24 allocs/op  (-20%),  -7.4% time
mapping     Unmarshal:         8 -> 5  allocs/op  (-37%),  -8.3% time
mapping     UnmarshalString:   4 -> 3  allocs/op  (-25%),  -7.5% time
mapping     UnmarshalStruct:  16 -> 15 allocs/op  (-6%)
mapping     DefaultValue:     30 -> 28 allocs/op  (-7%)

Both paths are exercised on every service start (config loading) and every HTTP request (httpx.Parse).

Notes

  • No behavior change: error messages are byte-identical; the opaque branch is equivalent to the existing single-key path in getValueWithChainedKeys.
  • go test ./core/mapping/... ./core/conf/... ./rest/... passes; go vet clean.

Two behavior-preserving changes that cut allocations on the config-load
and per-request request-parsing paths.

getValue: the form/path unmarshalers in rest/httpx use WithOpaqueKeys,
and opaque keys are never split on the delimiter, so they always resolve
to a single lookup. readKeys allocates a one-element []string{key} for
each such key, which getValueWithChainedKeys then reduces to a single
Valuer.Value call. Call Value directly and skip the slice allocation.

join: fullName is only used to build error messages, yet join() runs a
strings.Builder (and an allocating String() copy) for every named field.
When at most one segment is non-empty (top-level fields, where the parent
name is empty) return it directly. Error messages are byte-identical.

benchstat (Apple M1 Pro, count=8, all p<0.05):
  rest/httpx  ParseAuto:        30->24 allocs/op (-20%), -7.4% time
  mapping     Unmarshal:         8->5  allocs/op (-37%), -8.3% time
  mapping     UnmarshalString:   4->3  allocs/op (-25%), -7.5% time

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant