Skip to content

Decoder: decode the values held by interface values by their type, as encoding/json does - #663

Merged
goccy merged 3 commits into
masterfrom
perf/decoder-interface-type-cache
Sep 26, 2026
Merged

goccy merged 3 commits into
masterfrom
perf/decoder-interface-type-cache

Conversation

@goccy

@goccy goccy commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

The decoder of an interface value decided, for every value it decoded, how the value held by the interface value is decoded: by reflect.TypeOf, Kind and Elem for interface{}, and by reflect.ValueOf, NumMethod, Interface and up to three type assertions for an interface type with methods. That depends on the type of the held value only.

  • The decision is made once for a type. The decoder keeps it in a runtime.TypeCache, by the type of the held value for interface{} and by the itab for an interface type with methods ( one itab per held type for the interface type of the decoder ), with the decoder of the pointer type, which was looked up by DecoderOf at every value.
  • An interface type with methods is decoded as encoding/json does. It called whatever unmarshaler the held value implements, and failed otherwise. encoding/json decodes it as a value of interface{}: a pointer is decoded into what it points to as a value of the pointer type, with the unmarshalers of the pointer type, and null sets the interface value to nil. Any other value is a type error, which keeps the held value before Go 1.27; with Go 1.27, it is set to nil and the error is at the start of the value. A pointer which implements encoding.TextUnmarshaler reports the interface type before Go 1.27.
  • The bytes given to UnmarshalJSON are copied into a slice of their length. The copy was made by append, which goes through growslice ( 26% of the benchmark of the unmarshalers ).

Benchmarks

Two benchmarks decode into values set before the decoding: eight values of interface{} holding pointers to structs ( InterfacePointer ), and eight values of an interface type with methods holding pointers which implement json.Unmarshaler ( InterfaceUnmarshaler ).

amd64 CI, three machines ( EPYC 7763, Xeon 8370C, EPYC 9V74 ), master and the head each with two layouts ( a function added, to see the effect of the layout alone ):

benchmark master layouts head and its layouts
InterfacePointer -0.8% .. -0.1% -9.2% .. -7.0%
InterfaceUnmarshaler +0.5% .. +5.0% -15.1% .. -11.4%
TwitterBinding, TwitterGeneric, GitHubREST, MediumStruct, AnthropicMessageStream, CodeUnmarshal, UnmarshalString, UnmarshalInt64 within the band of the master layouts
SmallStruct ( which decodes no interface value ) -1.3% .. +0.4% -0.5% .. +1.4%

The benchmark gates of this pull request: decode means +0.00%, -0.10%, -0.62%; SmallStruct +0.16%.

Tests

TestDecodeTypeErrorsOfInterfaceValues compares with encoding/json the errors ( value, type, offset, struct, field, message ) and the decoded values of values of an interface type with methods and of interface{} holding nil, pointers implementing json.Unmarshaler or encoding.TextUnmarshaler, a pointer to a struct, a struct, and a nil pointer, for objects, null, strings, arrays, numbers and bools. TestUnmarshalIntoInterfaceValuesOfTypes decodes values of types which alternate in one field. All the tests pass with Go 1.21 to 1.27, and with Go 1.27 and GOEXPERIMENT=nojsonv2.

🤖 Generated with Claude Code

goccy and others added 3 commits September 26, 2026 11:32
…for its type

The decoder of an interface value found, for every value it decoded, how the
value it holds is decoded: into what a pointer points to, as a new value, or
by an unmarshaler, by reflect and by type assertions. That depends on the type
of the value only: it is found once for a type now, and kept by the decoder
by the type, or by the itab for an interface type with methods, with the
decoder of the pointer. The benchmarks decode into values of interface{}
which hold pointers, and into values of an interface type with methods which
implement json.Unmarshaler.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…/json does

A value of an interface type with methods was decoded by the unmarshaler it
implements, whatever it was, and was an error otherwise. encoding/json
decodes it as it decodes a value of interface{}: a pointer which it holds is
decoded into what it points to, as a value of the pointer type, whose
unmarshalers are used, and null sets the interface value to nil. Any other
value, which is not replaced as the one of interface{} is, is a type error:
before Go 1.27, the value is kept; with Go 1.27, it is set to nil and the
error is reported at the start of the value. The type error of a pointer
which implements encoding.TextUnmarshaler reports the interface type before
Go 1.27, as encoding/json does.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The copy of the bytes given to UnmarshalJSON was made by append, which goes
through growslice and rounds the capacity up: a slice of the exact length,
filled by copy, costs less.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 94.23077% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.78%. Comparing base (938cc9e) to head (a279e07).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #663      +/-   ##
==========================================
+ Coverage   86.47%   86.78%   +0.31%     
==========================================
  Files          73       73              
  Lines       11509    11485      -24     
==========================================
+ Hits         9952     9967      +15     
+ Misses       1557     1518      -39     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@goccy
goccy merged commit efb8230 into master Sep 26, 2026
29 checks passed
@goccy
goccy deleted the perf/decoder-interface-type-cache branch September 26, 2026 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants