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

1 participant