⚡ perf: SWAR-accelerated path lookup, fast scalar/slice decode, fewer encoder allocations - #83
Conversation
…decode Two decode-path optimizations building on the SWAR/SIMD support in gofiber/utils: - Precompute parsed paths for statically-resolvable keys (flat field aliases and dotted chains through non-pointer nested structs) into an immutable per-structInfo map at metadata build time. parsePathInfo case-folds the incoming key word-at-a-time with utils/v2/swar into a stack buffer and serves these keys with a single plain-map probe, bypassing the sync.Map path cache, per-segment lowercasing, and path parsing. Mixed-case keys hitting this path no longer store per-casing clones in the path cache. - Decode slice fields whose type is exactly []string, []int, []int64, []uint, []uint64, []float64, or []bool into a native Go slice assigned through the typed field pointer, eliminating reflect.MakeSlice and the per-element Index/Set calls of the generic path. Elements parse via the SWAR-backed utils parsers; semantics (comma splitting, zeroEmpty, all-or-nothing assignment, ConversionError details) match the generic path, which named slice/element types still use. Interleaved benchmark comparison (Go 1.25, linux/amd64): LargeStructDecode -13.5% SliceHeavyDecode -20.3% (new benchmark) SimpleStructDecode -2.8% encode, multipart, cache-miss and slice-index benchmarks unchanged Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ooKnSP4gG9tAd5rCAkyxZ
Second optimization sweep, plus tightened comments from the first: - parsePathInfo probes the direct-path map with the raw key first (keys are usually already lowercase) and only SWAR-case-folds on a miss, saving the fold for the common case and letting long lowercase keys hit the fast path too. - fieldInfo caches a fastKind for non-pointer builtin scalar fields with no unmarshaler or custom converter, letting decode set them directly and skip the converter/unmarshaler dispatch. Converter registration resets the cache, so the build-time decision stays valid. - Encoding into an empty dst map packs single values of distinct keys into one shared backing array (capped with three-index slices) instead of allocating a 1-element slice per field. A non-empty dst keeps the previous single-map-op append pattern, so accumulating encodes are unaffected. Interleaved benchmark comparison vs the previous commit: SimpleStructDecode -10..-17% LargeStructDecode ~-9% (high machine variance) EncodeFreshDst -12%, 7 -> 4 allocs/op (new benchmark) accumulate-encode, cache-miss, slice-index benchmarks unchanged Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ooKnSP4gG9tAd5rCAkyxZ
buildDirectPaths materialized every dotted chain through non-pointer nested structs, which is exponential in nesting depth for fan-out >= 2: a depth-18 fan-out-2 type made the first Decode take seconds and retain hundreds of MB in the decoder cache, re-triggered by every RegisterConverter/SetAliasTag reset. Cap nested entries at maxDirectPaths (512) per struct type; flat aliases are built first (linear in field count) so the cap can never crowd them out, and fields are iterated in declaration order for determinism. Keys beyond the cap fall back to the generic parser, which is semantically identical. The depth-18 reproduction drops from multi-second/167+ MB to ~16 ms/0.1 MB retained, with no change to the hot-path benchmarks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ooKnSP4gG9tAd5rCAkyxZ
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #83 +/- ##
==========================================
- Coverage 99.23% 99.17% -0.06%
==========================================
Files 4 4
Lines 1039 1205 +166
==========================================
+ Hits 1031 1195 +164
- Misses 4 5 +1
- Partials 4 5 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
📝 WalkthroughWalkthroughThe PR adds bounded direct-path metadata, native scalar and slice parsers, optimized decoding for eligible builtin types, and capped scratch storage for fresh-destination encoding. Tests and benchmarks cover lookup, fallback, conversion, allocation, and path limits. ChangesFast path processing
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant InputMap
participant parsePathInfo
participant decode
participant NativeParsers
InputMap->>parsePathInfo: provide field key
parsePathInfo-->>decode: return direct field metadata
decode->>NativeParsers: parse scalar or comma-separated slice values
NativeParsers-->>decode: return typed value or conversion error
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The native slice and fast scalar paths left the generic reflect loop, the pointer-field ZeroEmpty branch, and the encoder scratch append branch uncovered. Add tests exercising them through named slice types, pointer scalars, and duplicate aliases, and replace the unreachable v.Set fallback in decodeNativeSlice with a direct typed-pointer assignment (the dispatch switch guarantees the type). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ooKnSP4gG9tAd5rCAkyxZ
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 36b3f43c48
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Values stored in the fresh-dst scratch array pin the whole backing array's contents while any entry from that encode survives, so deleting a key holding a large string would not free it. Bound the exposure: values longer than maxScratchValueLen (64 bytes) get their own independently collectible slice, capping what a surviving entry can retain, while short values keep the single-allocation batching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ooKnSP4gG9tAd5rCAkyxZ
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aed8b34da3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A length check cannot bound retention: a short string can be a substring aliasing a much larger caller-owned buffer, which the shared scratch array would keep alive after its key is deleted. Batch only strings this package's own formatters allocate (bool/int/uint/float output); string fields and custom encoder results always get their own independently collectible slice. The scratch can then never retain caller memory, and the length cap bounds real bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ooKnSP4gG9tAd5rCAkyxZ
Summary
Performance pass over the decode and encode hot paths, building on the SWAR/SIMD support in
gofiber/utilsv2.1. Precomputed direct paths with SWAR key folding (
168e2ed)structInfomap at metadata build time.parsePathInfoserves these keys with a plain-map probe, bypassing thesync.Mappath cache, per-segment lowercasing, and path parsing.utils/v2/swarinto a stack buffer (no allocation). Mixed-case keys hitting this path no longer store per-casing clones in the path cache.[]string,[]int,[]int64,[]uint,[]uint64,[]float64, or[]booldecode into a native Go slice assigned through the typed field pointer, eliminatingreflect.MakeSliceand per-elementIndex/Setcalls. Named slice/element types keep the generic path; semantics (comma splitting,ZeroEmpty, all-or-nothing assignment,ConversionErrordetails) are unchanged.2. Raw-first probe, fast scalar decode, batched encoder allocations (
a0862f5)fieldInfocaches afastKindfor non-pointer builtin scalar fields with noTextUnmarshaleror custom converter, lettingdecodeset them directly and skip the converter/unmarshaler dispatch. Converter registration resets the cache, so the build-time decision stays valid.dstmap packs single values of distinct keys into one shared backing array (entries capped with three-index slices so later appends reallocate instead of overwriting a neighbor). A non-emptydstkeeps the previous single-map-op append pattern, so accumulating encodes are unaffected.3. Cap on direct-path precomputation (
ba480b4)Deep fan-out nesting has exponentially many dotted chains; uncapped precomputation made the first
Decodeof a depth-18 fan-out-2 type take seconds and retain hundreds of MB. Nested entries are now capped at 512 per struct type (flat aliases are built first and can never be crowded out); keys beyond the cap fall back to the generic parser, which is semantically identical. The reproduction drops to ~16 ms / 0.1 MB retained with no change to hot-path benchmarks.4. Review follow-ups (
36b3f43,aed8b34,2fa9a1f)ZeroEmpty, duplicate-alias encode) and removed an unreachable fallback branch.url.Valuesentry can never keep a deleted neighbor's caller-owned allocation alive.Benchmarks
Interleaved A/B runs vs
main(Go 1.25, linux/amd64, benchstat):Verification
-race;golangci-lintclean; project coverage 99.2% (patch 100%).ZeroEmpty, all-or-nothing,ConversionErrorindex), a deep fan-out type pinning the precomputation cap, and encoder scratch eligibility branches.main(case folding incl. non-ASCII and 64-byte boundaries, converter/alias-tag re-registration, embedded/promoted fields, multipart, 32-bit truncation guards, encoder scratch aliasing under recursion and duplicate keys) found no behavioral divergence.🤖 Generated with Claude Code
https://claude.ai/code/session_017ooKnSP4gG9tAd5rCAkyxZ