Skip to content

Commit 4b32ad8

Browse files
authored
Reuse coder options in MarshalEncode and UnmarshalDecode (#137)
When calling MarshalEncode and UnmarshalDecode, there are cases when we can directly reuse the coder.Struct options since it is identical to the resulting options if we were to merge the user-provided options with the coder-specific options in coder.Struct. Performance benefit on AMD Ryzen 9950X: UnmarshalDecodeOptions/None 57.8ns ± 1% 41.8ns ± 1% -27.66% (p=0.008 n=5+5) UnmarshalDecodeOptions/Same 63.1ns ± 1% 42.2ns ± 1% -33.09% (p=0.008 n=5+5) UnmarshalDecodeOptions/New 64.4ns ± 1% 64.1ns ± 2% ~ (p=0.690 n=5+5) MarshalEncodeOptions/None 55.6ns ± 3% 39.3ns ± 1% -29.26% (p=0.016 n=5+4) MarshalEncodeOptions/Same 60.0ns ± 2% 38.8ns ± 1% -35.41% (p=0.008 n=5+5) MarshalEncodeOptions/New 61.1ns ± 1% 61.2ns ± 1% ~ (p=1.000 n=5+5)
1 parent 5dee287 commit 4b32ad8

File tree

2 files changed

+79
-9
lines changed

2 files changed

+79
-9
lines changed

arshal.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,26 @@ import (
2121
// export exposes internal functionality of the "jsontext" package.
2222
var export = jsontext.Internal.Export(&internal.AllowInternalUse)
2323

24+
// mayReuseOpt reuses coderOpts if joining opts with the coderOpts
25+
// would produce the equivalent set of options.
26+
func mayReuseOpt(coderOpts *jsonopts.Struct, opts []Options) *jsonopts.Struct {
27+
switch len(opts) {
28+
// In the common case, the caller plumbs down options from the caller's caller,
29+
// which is usually the [jsonopts.Struct] constructed by the top-level arshal call.
30+
case 1:
31+
o, _ := opts[0].(*jsonopts.Struct)
32+
if o == coderOpts {
33+
return coderOpts
34+
}
35+
// If the caller provides no options, then just reuse the coder's options,
36+
// which should only contain encoding/decoding related flags.
37+
case 0:
38+
// TODO: This is buggy if coderOpts ever contains non-coder options.
39+
return coderOpts
40+
}
41+
return nil
42+
}
43+
2444
var structOptionsPool = &sync.Pool{New: func() any { return new(jsonopts.Struct) }}
2545

2646
func getStructOptions() *jsonopts.Struct {
@@ -192,13 +212,16 @@ func MarshalWrite(out io.Writer, in any, opts ...Options) (err error) {
192212
// they must have already been specified on the provided [jsontext.Encoder].
193213
// See [Marshal] for details about the conversion of a Go value into JSON.
194214
func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) (err error) {
195-
mo := getStructOptions()
196-
defer putStructOptions(mo)
197-
mo.Join(opts...)
198215
xe := export.Encoder(out)
199-
mo.CopyCoderOptions(&xe.Struct)
216+
mo := mayReuseOpt(&xe.Struct, opts)
217+
if mo == nil {
218+
mo = getStructOptions()
219+
defer putStructOptions(mo)
220+
mo.Join(opts...)
221+
mo.CopyCoderOptions(&xe.Struct)
222+
}
200223
err = marshalEncode(out, in, mo)
201-
if err != nil && xe.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
224+
if err != nil && mo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
202225
return internal.TransformMarshalError(in, err)
203226
}
204227
return err
@@ -435,11 +458,14 @@ func unmarshalFull(in *jsontext.Decoder, out any, uo *jsonopts.Struct) error {
435458
// The output must be a non-nil pointer.
436459
// See [Unmarshal] for details about the conversion of JSON into a Go value.
437460
func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) (err error) {
438-
uo := getStructOptions()
439-
defer putStructOptions(uo)
440-
uo.Join(opts...)
441461
xd := export.Decoder(in)
442-
uo.CopyCoderOptions(&xd.Struct)
462+
uo := mayReuseOpt(&xd.Struct, opts)
463+
if uo == nil {
464+
uo = getStructOptions()
465+
defer putStructOptions(uo)
466+
uo.Join(opts...)
467+
uo.CopyCoderOptions(&xd.Struct)
468+
}
443469
err = unmarshalDecode(in, out, uo)
444470
if err != nil && uo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
445471
return internal.TransformUnmarshalError(out, err)

arshal_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9299,3 +9299,47 @@ func TestUintSet(t *testing.T) {
92999299
}
93009300
}
93019301
}
9302+
9303+
// BenchmarkUnmarshalDecodeOptions is a minimal decode operation to measure
9304+
// the overhead options setup before the unmarshal operation.
9305+
func BenchmarkUnmarshalDecodeOptions(b *testing.B) {
9306+
var i int
9307+
in := new(bytes.Buffer)
9308+
dec := jsontext.NewDecoder(in)
9309+
makeBench := func(opts ...Options) func(*testing.B) {
9310+
return func(b *testing.B) {
9311+
for range b.N {
9312+
in.WriteString("0 ")
9313+
}
9314+
dec.Reset(in)
9315+
b.ResetTimer()
9316+
for range b.N {
9317+
UnmarshalDecode(dec, &i, opts...)
9318+
}
9319+
}
9320+
}
9321+
b.Run("None", makeBench())
9322+
b.Run("Same", makeBench(&export.Decoder(dec).Struct))
9323+
b.Run("New", makeBench(DefaultOptionsV2()))
9324+
}
9325+
9326+
// BenchmarkMarshalEncodeOptions is a minimal encode operation to measure
9327+
// the overhead of options setup before the marshal operation.
9328+
func BenchmarkMarshalEncodeOptions(b *testing.B) {
9329+
var i int
9330+
out := new(bytes.Buffer)
9331+
enc := jsontext.NewEncoder(out)
9332+
makeBench := func(opts ...Options) func(*testing.B) {
9333+
return func(b *testing.B) {
9334+
out.Reset()
9335+
enc.Reset(out)
9336+
b.ResetTimer()
9337+
for range b.N {
9338+
MarshalEncode(enc, &i, opts...)
9339+
}
9340+
}
9341+
}
9342+
b.Run("None", makeBench())
9343+
b.Run("Same", makeBench(&export.Encoder(enc).Struct))
9344+
b.Run("New", makeBench(DefaultOptionsV2()))
9345+
}

0 commit comments

Comments
 (0)