Skip to content

⚡ perf: SWAR-accelerated path lookup, fast scalar/slice decode, fewer encoder allocations - #83

Merged
ReneWerner87 merged 6 commits into
mainfrom
claude/schema-performance-optimization-ivx13a
Aug 10, 2026
Merged

⚡ perf: SWAR-accelerated path lookup, fast scalar/slice decode, fewer encoder allocations#83
ReneWerner87 merged 6 commits into
mainfrom
claude/schema-performance-optimization-ivx13a

Conversation

@gaby

@gaby gaby commented Aug 10, 2026

Copy link
Copy Markdown
Member

Summary

Performance pass over the decode and encode hot paths, building on the SWAR/SIMD support in gofiber/utils v2.

1. Precomputed direct paths with SWAR key folding (168e2ed)

  • Parsed paths for statically-resolvable keys — flat field aliases and dotted chains through non-pointer nested structs — are precomputed into an immutable per-structInfo map at metadata build time. parsePathInfo serves these keys with a plain-map probe, bypassing the sync.Map path cache, per-segment lowercasing, and path parsing.
  • Keys are matched case-insensitively by folding them word-at-a-time with utils/v2/swar into a stack buffer (no allocation). Mixed-case keys hitting this path no longer store per-casing clones in the path cache.
  • Slice fields typed exactly []string, []int, []int64, []uint, []uint64, []float64, or []bool decode into a native Go slice assigned through the typed field pointer, eliminating reflect.MakeSlice and per-element Index/Set calls. Named slice/element types keep the generic path; semantics (comma splitting, ZeroEmpty, all-or-nothing assignment, ConversionError details) are unchanged.

2. Raw-first probe, fast scalar decode, batched encoder allocations (a0862f5)

  • The direct-path map is probed with the raw key first (keys are usually already lowercase); the SWAR fold runs only on a miss.
  • fieldInfo caches a fastKind for non-pointer builtin scalar fields with no TextUnmarshaler 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 (entries capped with three-index slices so later appends reallocate instead of overwriting a neighbor). A non-empty dst keeps 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 Decode of 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)

  • Restored coverage of the generic paths the fast paths bypass (named slice types, pointer-scalar ZeroEmpty, duplicate-alias encode) and removed an unreachable fallback branch.
  • Hardened the encoder scratch against memory retention (raised by Codex review): only strings allocated by this package's own formatters (bool/int/uint/float output, ≤64 bytes) are batched in the shared array — string fields and custom encoder results always get their own independently collectible slice, so a surviving url.Values entry can never keep a deleted neighbor's caller-owned allocation alive.

Benchmarks

Interleaved A/B runs vs main (Go 1.25, linux/amd64, benchstat):

Benchmark Change
SimpleStructDecode −13% to −20%
LargeStructDecode ~−20%
SliceHeavyDecode (new) ~−20%, fewer B/op
EncodeFreshDst (new) −12%, 7 → 5 allocs/op
MixedCaseKeyDecode (new) fast path, no path-cache growth
accumulate-encode, cache-miss, slice-index, multipart unchanged

Verification

  • Full test suite passes, including -race; golangci-lint clean; project coverage 99.2% (patch 100%).
  • New regression tests: direct-path case-fold parity (mixed-case flat and nested keys), >64-byte key and pointer-chain fallbacks, bare slice-of-structs aliases staying invalid, native-slice semantics (comma splitting, ZeroEmpty, all-or-nothing, ConversionError index), a deep fan-out type pinning the precomputation cap, and encoder scratch eligibility branches.
  • Extensive differential testing against 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

claude added 3 commits August 9, 2026 23:18
…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
@gaby
gaby requested a review from a team as a code owner August 10, 2026 02:15
@gaby
gaby requested review from ReneWerner87, efectn and sixcolors and removed request for a team August 10, 2026 02:15
@gaby gaby changed the title perf: precompute direct paths with SWAR key folding and native slice decode ⚡ perf: precompute direct paths with SWAR key folding and native slice decode Aug 10, 2026
@gaby gaby changed the title ⚡ perf: precompute direct paths with SWAR key folding and native slice decode ⚡ perf: SWAR-accelerated path lookup, fast scalar/slice decode, fewer encoder allocations Aug 10, 2026
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.17%. Comparing base (650fa90) to head (2fa9a1f).

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     
Flag Coverage Δ
unittests 99.17% <100.00%> (-0.06%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Fast path processing

Layer / File(s) Summary
Direct-path metadata and lookup
cache.go, cache_test.go, fastpath_test.go
structInfo precomputes bounded flat and nested paths. parsePathInfo checks raw and case-folded direct paths before generic parsing. Tests cover aliases, fallbacks, detached cache keys, and path limits.
Native scalar and slice decoding
converter.go, decoder.go, fastpath_test.go
Native parsers handle strings, integers, unsigned integers, floats, and booleans. Exact builtin slices support comma splitting, empty values, indexed conversion errors, and atomic assignment. Named slices retain reflective decoding.
Fresh-destination encoding storage
encoder.go, fastpath_test.go
Encoding shares capped scratch storage for short numeric and boolean values in initially empty destinations. Strings, custom encoder results, long values, and populated destinations retain independent or append behavior.

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
Loading

Possibly related PRs

  • gofiber/schema#76: Both PRs modify decode and conversion fast paths, including decodeBuiltinSlice and cached path metadata.

Suggested reviewers: efectn, sixcolors, renewerner87

Poem

A rabbit checks each path with care,
Native slices hop through the air.
Scratch values stay within their pen,
Tests trace every path again.
Fast fields bloom beneath the sun.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main performance changes to path lookup, scalar and slice decoding, and encoder allocations.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/schema-performance-optimization-ivx13a

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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
@gaby

gaby commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread encoder.go
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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread encoder.go Outdated
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
@ReneWerner87
ReneWerner87 merged commit c9a4f6f into main Aug 10, 2026
21 of 22 checks passed
@ReneWerner87
ReneWerner87 deleted the claude/schema-performance-optimization-ivx13a branch August 10, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants