Skip to content

perf: precompute static field facts in encoder plans#1106

Open
pelletier wants to merge 1 commit into
perf/encoder-type-propsfrom
perf/encoder-plan-facts
Open

perf: precompute static field facts in encoder plans#1106
pelletier wants to merge 1 commit into
perf/encoder-type-propsfrom
perf/encoder-plan-facts

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 #1105.

What

Struct entries re-derived per encode what their plan already knows statically:

  • whether the key needs quoting — a byte scan per key, and again for every parent part of every nested table header. Plan fields carry bareKey; key-stack parts carry their quoting need, computed once at push (keyStackPart).
  • the entry class, when the field type fully determines it (staticFieldFacts). Interfaces and pointers resolve dynamically, slices of table-like elements switch on emptiness, and the Marshaler mode re-classifies dynamically (its shape depends on runtime content) — those keep the runtime path.
  • the type encoding properties (props/propsKnown), feeding appendValueProps directly instead of a cache lookup per value.

Entries also slim down: options (the widest part of the entry) becomes a pointer into the shared read-only plan — with the Marshaler rawShape moving onto the entry itself so nothing ever writes through the shared pointer — and single-index (non-embedded) field access skips the index-walk loop.

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: -1.74%
  • Marshal/ReferenceFile/struct: -26.91%
  • Marshal/SimpleDocument/struct: -5.81%
  • RealWorldViperWrite: -4.73%
  • Unmarshal/SimpleDocument/struct: -4.01%
  • Unmarshal/SimpleDocument/map: -3.08%
  • Marshal/HugoFrontMatter: -1.59%
  • RealWorldContainerdConfig: +1.56%
  • RealWorldPyproject: +1.68%
  • UnmarshalDataset/canada: +2.18%
  • Marshal/SimpleDocument/map: +5.25%
Full benchstat (sec/op, allocs/op)

sec/op

UnmarshalDataset/config  10.34m ± 2%  10.32m ± 1%  ~ (p=0.684 n=10)
UnmarshalDataset/canada  23.46m ± 4%  23.97m ± 1%  +2.18% (p=0.029 n=10)
UnmarshalDataset/citm_catalog  15.17m ± 1%  15.07m ± 1%  -0.66% (p=0.043 n=10)
UnmarshalDataset/twitter  3.600m ± 1%  3.589m ± 0%  ~ (p=0.089 n=10)
UnmarshalDataset/code  32.89m ± 1%  33.00m ± 3%  ~ (p=0.052 n=10)
UnmarshalDataset/example  77.58µ ± 1%  77.18µ ± 1%  ~ (p=0.123 n=10)
Unmarshal/SimpleDocument/struct  335.6n ± 1%  322.1n ± 2%  -4.01% (p=0.000 n=10)
Unmarshal/SimpleDocument/map  436.1n ± 2%  422.6n ± 1%  -3.08% (p=0.000 n=10)
Unmarshal/ReferenceFile/struct  39.70µ ± 3%  38.95µ ± 3%  ~ (p=0.289 n=10)
Unmarshal/ReferenceFile/map  31.61µ ± 4%  31.57µ ± 1%  ~ (p=0.481 n=10)
Unmarshal/HugoFrontMatter  5.696µ ± 3%  5.719µ ± 1%  ~ (p=0.796 n=10)
Marshal/SimpleDocument/struct  359.5n ± 5%  338.6n ± 2%  -5.81% (p=0.000 n=10)
Marshal/SimpleDocument/map  514.1n ± 1%  541.2n ± 1%  +5.25% (p=0.000 n=10)
Marshal/ReferenceFile/struct  17.77µ ± 4%  12.99µ ± 1%  -26.91% (p=0.000 n=10)
Marshal/ReferenceFile/map  32.04µ ± 4%  32.49µ ± 3%  ~ (p=0.853 n=10)
Marshal/HugoFrontMatter  6.237µ ± 3%  6.138µ ± 4%  -1.59% (p=0.035 n=10)
RealWorldContainerdConfig  39.62µ ± 2%  40.24µ ± 1%  +1.56% (p=0.043 n=10)
RealWorldViperRead  8.243µ ± 3%  8.289µ ± 2%  ~ (p=0.494 n=10)
RealWorldViperWrite  9.958µ ± 2%  9.487µ ± 2%  -4.73% (p=0.000 n=10)
RealWorldHugoFrontMatterBatch  94.23µ ± 3%  95.73µ ± 2%  ~ (p=0.089 n=10)
RealWorldGitleaksRules  64.59µ ± 1%  64.32µ ± 1%  ~ (p=0.315 n=10)
RealWorldPyproject  14.92µ ± 1%  15.17µ ± 2%  +1.68% (p=0.023 n=10)
RealWorldGolangciStrict  11.82µ ± 1%  11.83µ ± 0%  ~ (p=0.753 n=10)
geomean  44.48µ  43.71µ  -1.74%

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=1.000 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  48.00 ± 0%  48.00 ± 0%  ~ (p=1.000 n=10) ¹
geomean  211.1  211.1  +0.00%
¹ all samples are equal

🤖 Generated with Claude Code

Struct entries re-derived per encode what their plan already knows:

- whether the key needs quoting (a byte scan per key, and again for
  every parent part of every nested table header — key-stack parts now
  carry their quoting need, computed once at push);
- the entry class, when the field type fully determines it (interfaces
  and pointers resolve dynamically, and slices of table-like elements
  switch on emptiness, so those still classify at runtime — as does
  everything when the Marshaler interface is enabled);
- the type encoding properties, previously an interface-hashed cache
  lookup per value.

Entries also slim down: the options (the widest part) become a pointer
into the shared plan, with the Marshaler rawShape moving onto the entry
itself, and single-index (non-embedded) field access skips the
index-walk loop.

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