Skip to content

perf: decode scalar key-values of reflection targets without an AST#1103

Open
pelletier wants to merge 1 commit into
perf/strict-lazy-keysfrom
perf/fused-struct
Open

perf: decode scalar key-values of reflection targets without an AST#1103
pelletier wants to merge 1 commit into
perf/strict-lazy-keysfrom
perf/fused-struct

Conversation

@pelletier

Copy link
Copy Markdown
Owner

Splitting #1088: this PR carries one optimization technique so its impact and review surface stay isolated. Stacked on #1102: merge the decode chain in order.

What

Reflection targets (structs, typed maps, ...) parsed every expression into the AST arena. Scalar key-values — the bulk of a document — now scan their key and value directly (fusedStructDocument / fusedStructKeyVal) and dispatch through the existing descend/assign machinery via path parts carrying the scanned bytes and ranges (pathPart.data/rng).

What deliberately stays on the expression parser:

  • Table headers and container values — a two-function cursor bridge (SetCursor/Cursor) hands the document position to Parser.NextExpression and back, so array-table bookkeeping, seen-tracking, and error rendering are byte-identical. Container values are validated by the per-value tracker with the same rules.
  • The unmarshaler-interface mode keeps the full AST loop: captures need the raw expression ranges.

Strict mode reports missing fields through parts with the same key spans an AST node would produce (MissingFieldParts), so StrictMissingError output is unchanged.

Impact

Benchmarked on a dedicated linux/amd64 spot VM (t2d), go1.26.4, interleaved A/B vs the base of this PR, benchstat over 10 samples per side:

  • sec/op geomean: -7.31%
  • Marshal/SimpleDocument/struct: -26.87%
  • UnmarshalDataset/code: -20.42%
  • Marshal/SimpleDocument/map: -20.33%
  • UnmarshalDataset/example: -13.26%
  • Unmarshal/SimpleDocument/map: -9.81%
  • Unmarshal/SimpleDocument/struct: -9.53%
  • RealWorldGolangciStrict: -2.25%
  • RealWorldViperWrite: +2.35%
  • Unmarshal/ReferenceFile/struct: +5.64%
  • Unmarshal/HugoFrontMatter: +9.49%
  • Marshal/ReferenceFile/struct: +11.05%
Full benchstat (sec/op, allocs/op)

sec/op

UnmarshalDataset/config  17.55m ± 32%  12.67m ± 47%  ~ (p=0.971 n=10)
UnmarshalDataset/canada  33.87m ± 20%  29.60m ± 19%  ~ (p=0.631 n=10)
UnmarshalDataset/citm_catalog  15.89m ± 28%  11.67m ± 50%  ~ (p=0.315 n=10)
UnmarshalDataset/twitter  5.768m ± 13%  5.016m ± 35%  ~ (p=0.218 n=10)
UnmarshalDataset/code  51.29m ± 17%  40.82m ± 28%  -20.42% (p=0.043 n=10)
UnmarshalDataset/example  116.8µ ± 11%  101.3µ ± 12%  -13.26% (p=0.005 n=10)
Unmarshal/SimpleDocument/struct  393.4n ±  3%  355.9n ±  3%  -9.53% (p=0.000 n=10)
Unmarshal/SimpleDocument/map  636.1n ± 15%  573.8n ±  3%  -9.81% (p=0.029 n=10)
Unmarshal/ReferenceFile/struct  40.00µ ±  5%  42.26µ ±  2%  +5.64% (p=0.001 n=10)
Unmarshal/ReferenceFile/map  40.09µ ±  4%  39.43µ ±  4%  ~ (p=0.052 n=10)
Unmarshal/HugoFrontMatter  7.673µ ±  6%  8.401µ ± 10%  +9.49% (p=0.019 n=10)
Marshal/SimpleDocument/struct  676.2n ±  3%  494.5n ± 41%  -26.87% (p=0.023 n=10)
Marshal/SimpleDocument/map  909.3n ±  3%  724.4n ± 25%  -20.33% (p=0.001 n=10)
Marshal/ReferenceFile/struct  22.24µ ± 13%  24.70µ ±  4%  +11.05% (p=0.008 n=10)
Marshal/ReferenceFile/map  43.76µ ±  6%  44.14µ ±  1%  ~ (p=0.529 n=10)
Marshal/HugoFrontMatter  9.335µ ±  5%  9.692µ ±  6%  ~ (p=0.190 n=10)
RealWorldContainerdConfig  47.13µ ±  9%  47.01µ ±  5%  ~ (p=0.579 n=10)
RealWorldViperRead  11.41µ ±  7%  11.39µ ±  4%  ~ (p=0.684 n=10)
RealWorldViperWrite  13.43µ ±  3%  13.75µ ±  6%  +2.35% (p=0.002 n=10)
RealWorldHugoFrontMatterBatch  132.8µ ±  3%  133.1µ ±  7%  ~ (p=0.631 n=10)
RealWorldGitleaksRules  79.00µ ±  1%  77.62µ ±  5%  ~ (p=0.143 n=10)
RealWorldPyproject  20.86µ ±  9%  21.19µ ±  5%  ~ (p=0.481 n=10)
RealWorldGolangciStrict  13.58µ ± 11%  13.27µ ± 11%  -2.25% (p=0.035 n=10)
geomean  61.11µ  56.64µ  -7.31%

allocs/op

UnmarshalDataset/config  70.19k ± 0%  70.19k ± 0%  ~ (p=1.000 n=10)
UnmarshalDataset/canada  223.2k ± 0%  223.2k ± 0%  ~ (p=1.000 n=10) ¹
UnmarshalDataset/citm_catalog  49.98k ± 0%  49.98k ± 0%  ~ (p=1.000 n=10) ¹
UnmarshalDataset/twitter  15.78k ± 0%  15.78k ± 0%  ~ (p=1.000 n=10) ¹
UnmarshalDataset/code  129.5k ± 0%  129.5k ± 0%  ~ (p=0.211 n=10)
UnmarshalDataset/example  399.0 ± 0%  399.0 ± 0%  ~ (p=1.000 n=10) ¹
Unmarshal/SimpleDocument/struct  2.000 ± 0%  2.000 ± 0%  ~ (p=1.000 n=10) ¹
Unmarshal/SimpleDocument/map  5.000 ± 0%  5.000 ± 0%  ~ (p=1.000 n=10) ¹
Unmarshal/ReferenceFile/struct  83.00 ± 0%  83.00 ± 0%  ~ (p=1.000 n=10) ¹
Unmarshal/ReferenceFile/map  194.0 ± 0%  194.0 ± 0%  ~ (p=1.000 n=10) ¹
Unmarshal/HugoFrontMatter  54.00 ± 0%  54.00 ± 0%  ~ (p=1.000 n=10) ¹
Marshal/SimpleDocument/struct  4.000 ± 0%  4.000 ± 0%  ~ (p=1.000 n=10) ¹
Marshal/SimpleDocument/map  4.000 ± 0%  4.000 ± 0%  ~ (p=1.000 n=10) ¹
Marshal/ReferenceFile/struct  4.000 ± 0%  4.000 ± 0%  ~ (p=1.000 n=10) ¹
Marshal/ReferenceFile/map  91.00 ± 0%  91.00 ± 0%  ~ (p=1.000 n=10) ¹
Marshal/HugoFrontMatter  20.00 ± 0%  20.00 ± 0%  ~ (p=1.000 n=10) ¹
RealWorldContainerdConfig  144.0 ± 0%  144.0 ± 0%  ~ (p=1.000 n=10) ¹
RealWorldViperRead  65.00 ± 0%  65.00 ± 0%  ~ (p=1.000 n=10) ¹
RealWorldViperWrite  33.00 ± 0%  33.00 ± 0%  ~ (p=1.000 n=10) ¹
RealWorldHugoFrontMatterBatch  820.0 ± 0%  820.0 ± 0%  ~ (p=1.000 n=10) ¹
RealWorldGitleaksRules  259.0 ± 0%  259.0 ± 0%  ~ (p=1.000 n=10) ¹
RealWorldPyproject  142.0 ± 0%  142.0 ± 0%  ~ (p=1.000 n=10) ¹
RealWorldGolangciStrict  34.00 ± 0%  34.00 ± 0%  ~ (p=1.000 n=10) ¹
geomean  207.9  207.9  +0.00%
¹ all samples are equal

This is a decode-only change: the Marshal/* rows moving in both directions are code-alignment noise (the encoder is untouched byte-for-byte). Within decode, scalar-heavy documents win big (code −20%, example −13%, SimpleDocument −10%); ReferenceFile/struct/HugoFrontMatter give some back on this host — table-header-heavy shapes where the cursor bridge round-trips dominate — and net out ahead across the chain (see the combined numbers in the #1088 close-out).

🤖 Generated with Claude Code

Reflection targets (structs, typed maps, ...) parsed every expression
into the AST arena. Scalar key-values — the bulk of a document — now
scan their key and value directly and dispatch through the existing
descend/assign machinery via path parts carrying the scanned bytes and
ranges.

Table headers and container values still go through the expression
parser (a two-function cursor bridge hands the document position back
and forth), so array-table bookkeeping and error rendering are
identical; container values are validated by the per-value tracker with
the same rules, and strict mode reports missing fields through parts
with the same key spans an AST node would produce.

The unmarshaler-interface mode keeps the full AST loop: captures need
the raw expression ranges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pelletier pelletier added the performance Issue related to a performance problem or pull request improving performance. label Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Issue related to a performance problem or pull request improving performance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant