From e3c0c265f18412fbb072ca08a77e5d3dd65e3c28 Mon Sep 17 00:00:00 2001 From: Phil Pearl Date: Thu, 13 Aug 2026 17:14:58 +0100 Subject: [PATCH] encoding/json/v2: reduce allocations marshalling when a pointer to an omitzero field implements IsZero Where I work we have structs with many small fields with IsZero methods. Marshalling these structs with json/v2 causes many allocations, one per such field. I tracked the allocation down to the code that checks for and calls the IsZero method. Calling reflect.Value.Interface{} causes an allocation when the value is not a pointer type. Removing the excess allocation is as easy as switching the order of two lines in a switch statement so that the case that tries to use a pointer to the field is tried first. Before: BenchmarkMarshal/OmitZeroMethod-10 14441822 83.81 ns/op 23.86 MB/s 16 B/op 2 allocs/op After BenchmarkMarshal/OmitZeroMethod-10 14114629 80.38 ns/op 24.88 MB/s 8 B/op 1 allocs/op Fixes #80868 Change-Id: I3c1da4946e16a94e70658cdfa02e2b5e29b5c6a5 --- src/encoding/json/v2/bench_test.go | 25 +++++++++++++++++++++++++ src/encoding/json/v2/fields.go | 4 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/encoding/json/v2/bench_test.go b/src/encoding/json/v2/bench_test.go index 7672acb1d1c93f..4733bbfc6af3eb 100644 --- a/src/encoding/json/v2/bench_test.go +++ b/src/encoding/json/v2/bench_test.go @@ -278,6 +278,12 @@ var arshalTestdata = []struct { raw: []byte(`"2006-01-02T22:04:05Z"`), val: addr(time.Unix(1136239445, 0).UTC()), new: func() any { return new(time.Time) }, +}, { + name: "OmitZeroMethod", + raw: []byte(`{}`), + val: new(omitZeroStruct), + new: func() any { return new(omitZeroStruct) }, + skipV1: true, }} type textArshaler struct{ _ [4]int } @@ -317,6 +323,25 @@ func (*jsonArshalerV2) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return err } +type omitZeroStruct struct { + A zeroAlways `json:"a,omitzero"` + B zeroAlwaysPtr `json:"b,omitzero"` +} + +type zeroAlways struct { + // Just so the struct has some size to allocate + A int +} + +func (zeroAlways) IsZero() bool { return true } + +type zeroAlwaysPtr struct { + // Just so the struct has some size to allocate + A int +} + +func (*zeroAlwaysPtr) IsZero() bool { return true } + func TestBenchmarkUnmarshal(t *testing.T) { runUnmarshal(t) } func BenchmarkUnmarshal(b *testing.B) { runUnmarshal(b) } diff --git a/src/encoding/json/v2/fields.go b/src/encoding/json/v2/fields.go index f67a9ecb08f237..860e34170ccfbc 100644 --- a/src/encoding/json/v2/fields.go +++ b/src/encoding/json/v2/fields.go @@ -229,10 +229,10 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError) // Avoid panics calling IsZero on nil pointer. return va.IsNil() || va.Interface().(isZeroer).IsZero() } - case sf.Type.Implements(isZeroerType): - f.isZero = func(va addressableValue) bool { return va.Interface().(isZeroer).IsZero() } case reflect.PointerTo(sf.Type).Implements(isZeroerType): f.isZero = func(va addressableValue) bool { return va.Addr().Interface().(isZeroer).IsZero() } + case sf.Type.Implements(isZeroerType): + f.isZero = func(va addressableValue) bool { return va.Interface().(isZeroer).IsZero() } } // Provide a function that can determine whether the value would