Skip to content

perf: classify encoder table entries once#1104

Open
pelletier wants to merge 1 commit into
v2from
perf/encoder-classify
Open

perf: classify encoder table entries once#1104
pelletier wants to merge 1 commit into
v2from
perf/encoder-classify

Conversation

@pelletier

Copy link
Copy Markdown
Owner

Splitting #1088: this PR carries one optimization technique so its impact and review surface stay isolated. First of the encoder chain.

What

Deciding whether an entry is a key-value, a table, or an array of tables requires resolving the value's type — and for arrays, resolving the type of every element. That decision was re-derived by each encoding pass (and a third time by SetOmitEmptySuperTables's super-table check), up to three times per entry.

Entries now carry an entryClass, computed at most once and cached on the entry: the first pass classifies while emitting key-values, the second pass routes on the stored class, and onlySubTables reuses whatever the entries already know (classOf computes lazily for the entries it sees first). entryIsTable disappears.

Marshaler-shaped entries keep their rawShape routing and are assigned their class by it, so the opt-in unstable.Marshaler semantics are untouched.

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: -2.45%
  • Marshal/HugoFrontMatter: -9.85%
  • Marshal/ReferenceFile/struct: -9.63%
  • RealWorldViperWrite: -8.13%
  • Marshal/SimpleDocument/map: -7.63%
  • Marshal/SimpleDocument/struct: -7.23%
  • Marshal/ReferenceFile/map: -7.02%
Full benchstat (sec/op, allocs/op)

sec/op

UnmarshalDataset/config  10.24m ± 1%  10.27m ± 1%  ~ (p=0.529 n=10)
UnmarshalDataset/canada  23.26m ± 1%  23.16m ± 1%  ~ (p=0.912 n=10)
UnmarshalDataset/citm_catalog  14.91m ± 1%  14.79m ± 1%  ~ (p=0.143 n=10)
UnmarshalDataset/twitter  3.572m ± 0%  3.566m ± 0%  ~ (p=0.075 n=10)
UnmarshalDataset/code  33.32m ± 1%  33.45m ± 1%  ~ (p=0.631 n=10)
UnmarshalDataset/example  77.10µ ± 0%  77.01µ ± 2%  ~ (p=0.529 n=10)
Unmarshal/SimpleDocument/struct  332.0n ± 1%  328.6n ± 3%  ~ (p=0.159 n=10)
Unmarshal/SimpleDocument/map  423.1n ± 1%  423.4n ± 1%  ~ (p=0.542 n=10)
Unmarshal/ReferenceFile/struct  39.04µ ± 2%  38.87µ ± 2%  ~ (p=0.853 n=10)
Unmarshal/ReferenceFile/map  31.21µ ± 1%  31.15µ ± 1%  ~ (p=0.853 n=10)
Unmarshal/HugoFrontMatter  5.602µ ± 1%  5.615µ ± 1%  ~ (p=0.448 n=10)
Marshal/SimpleDocument/struct  397.4n ± 1%  368.7n ± 3%  -7.23% (p=0.000 n=10)
Marshal/SimpleDocument/map  592.4n ± 1%  547.2n ± 2%  -7.63% (p=0.000 n=10)
Marshal/ReferenceFile/struct  20.44µ ± 1%  18.47µ ± 4%  -9.63% (p=0.000 n=10)
Marshal/ReferenceFile/map  38.89µ ± 2%  36.16µ ± 3%  -7.02% (p=0.000 n=10)
Marshal/HugoFrontMatter  7.941µ ± 3%  7.158µ ± 2%  -9.85% (p=0.000 n=10)
RealWorldContainerdConfig  39.67µ ± 1%  39.26µ ± 2%  ~ (p=0.089 n=10)
RealWorldViperRead  8.241µ ± 1%  8.210µ ± 0%  ~ (p=0.160 n=10)
RealWorldViperWrite  12.67µ ± 3%  11.64µ ± 3%  -8.13% (p=0.000 n=10)
RealWorldHugoFrontMatterBatch  93.86µ ± 2%  92.58µ ± 1%  -1.36% (p=0.000 n=10)
RealWorldGitleaksRules  63.43µ ± 3%  63.74µ ± 1%  ~ (p=0.971 n=10)
RealWorldPyproject  14.85µ ± 1%  14.71µ ± 1%  -0.96% (p=0.029 n=10)
RealWorldGolangciStrict  11.73µ ± 1%  11.75µ ± 1%  ~ (p=0.753 n=10)
geomean  46.26µ  45.13µ  -2.45%

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

Deciding whether an entry is a key-value, a table, or an array of
tables requires resolving the value's type — and for arrays, the type
of every element. That decision was re-derived by each encoding pass
(and again by the super-table check), up to three times per entry.

Entries now carry an entryClass, computed at most once and cached:
the first pass classifies while emitting key-values, the second pass
routes on the stored class, and onlySubTables reuses whatever the
entries already know.

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