Skip to content

Commit 11b2853

Browse files
mvdanbradfitz
authored andcommitted
encoding/json: don't reuse slice elements when decoding
The previous behavior directly contradicted the docs that have been in place for years: To unmarshal a JSON array into a slice, Unmarshal resets the slice length to zero and then appends each element to the slice. We could use reflect.New to create a new element and reflect.Append to then append it to the destination slice, but benchmarks have shown that reflect.Append is very slow compared to the code that manually grows a slice in this file. Instead, if we're decoding into an element that came from the original backing array, zero it before decoding into it. We're going to be using the CodeDecoder benchmark, as it has a slice of struct pointers that's decoded very often. Note that we still reuse existing values from arrays being decoded into, as the documentation agrees with the existing implementation in that case: To unmarshal a JSON array into a Go array, Unmarshal decodes JSON array elements into corresponding Go array elements. The numbers with the benchmark as-is might seem catastrophic, but that's only because the benchmark is decoding into the same variable over and over again. Since the old decoder was happy to reuse slice elements, it would save a lot of allocations by not having to zero and re-allocate said elements: name old time/op new time/op delta CodeDecoder-8 10.4ms ± 1% 10.9ms ± 1% +4.41% (p=0.000 n=10+10) name old speed new speed delta CodeDecoder-8 186MB/s ± 1% 178MB/s ± 1% -4.23% (p=0.000 n=10+10) name old alloc/op new alloc/op delta CodeDecoder-8 2.19MB ± 0% 3.59MB ± 0% +64.09% (p=0.000 n=10+10) name old allocs/op new allocs/op delta CodeDecoder-8 76.8k ± 0% 92.7k ± 0% +20.71% (p=0.000 n=10+10) We can prove this by moving 'var r codeResponse' into the loop, so that the benchmark no longer reuses the destination pointer. And sure enough, we no longer see the slow-down caused by the extra allocations: name old time/op new time/op delta CodeDecoder-8 10.9ms ± 0% 10.9ms ± 1% -0.37% (p=0.043 n=10+10) name old speed new speed delta CodeDecoder-8 177MB/s ± 0% 178MB/s ± 1% +0.37% (p=0.041 n=10+10) name old alloc/op new alloc/op delta CodeDecoder-8 3.59MB ± 0% 3.59MB ± 0% ~ (p=0.780 n=10+10) name old allocs/op new allocs/op delta CodeDecoder-8 92.7k ± 0% 92.7k ± 0% ~ (all equal) I believe that it's useful to leave the benchmarks as they are now, because the decoder does reuse memory in some cases. For example, existing map elements are reused. However, subtle changes like this one need to be benchmarked carefully. Finally, add a couple of tests involving both a slice and an array of structs. Fixes #21092. Change-Id: I8b1194f25e723a31abd146fbfe9428ac10c1389d Reviewed-on: https://go-review.googlesource.com/c/go/+/191783 Reviewed-by: Ian Lance Taylor <iant@golang.org> Run-TryBot: Ian Lance Taylor <iant@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
1 parent 8516229 commit 11b2853

File tree

2 files changed

+36
-19
lines changed

2 files changed

+36
-19
lines changed

src/encoding/json/decode.go

+22-18
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,7 @@ func (d *decodeState) unmarshal(v interface{}) error {
177177
d.scanWhile(scanSkipSpace)
178178
// We decode rv not rv.Elem because the Unmarshaler interface
179179
// test must be applied at the top level of the value.
180-
err := d.value(rv)
181-
if err != nil {
180+
if err := d.value(rv); err != nil {
182181
return d.addErrorContext(err)
183182
}
184183
return d.savedError
@@ -525,6 +524,7 @@ func (d *decodeState) array(v reflect.Value) error {
525524
return nil
526525
}
527526
v = pv
527+
initialSliceCap := 0
528528

529529
// Check type of target.
530530
switch v.Kind() {
@@ -541,8 +541,9 @@ func (d *decodeState) array(v reflect.Value) error {
541541
d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)})
542542
d.skip()
543543
return nil
544-
case reflect.Array, reflect.Slice:
545-
break
544+
case reflect.Slice:
545+
initialSliceCap = v.Cap()
546+
case reflect.Array:
546547
}
547548

548549
i := 0
@@ -553,7 +554,6 @@ func (d *decodeState) array(v reflect.Value) error {
553554
break
554555
}
555556

556-
// Get element of array, growing if necessary.
557557
if v.Kind() == reflect.Slice {
558558
// Grow slice if necessary
559559
if i >= v.Cap() {
@@ -569,19 +569,22 @@ func (d *decodeState) array(v reflect.Value) error {
569569
v.SetLen(i + 1)
570570
}
571571
}
572-
572+
var into reflect.Value
573573
if i < v.Len() {
574-
// Decode into element.
575-
if err := d.value(v.Index(i)); err != nil {
576-
return err
577-
}
578-
} else {
579-
// Ran out of fixed array: skip.
580-
if err := d.value(reflect.Value{}); err != nil {
581-
return err
574+
into = v.Index(i)
575+
if i < initialSliceCap {
576+
// Reusing an element from the slice's original
577+
// backing array; zero it before decoding.
578+
into.Set(reflect.Zero(v.Type().Elem()))
582579
}
583580
}
584581
i++
582+
// Note that we decode the value even if we ran past the end of
583+
// the fixed array. In that case, we decode into an empty value
584+
// and do nothing with it.
585+
if err := d.value(into); err != nil {
586+
return err
587+
}
585588

586589
// Next token must be , or ].
587590
if d.opcode == scanSkipSpace {
@@ -597,16 +600,17 @@ func (d *decodeState) array(v reflect.Value) error {
597600

598601
if i < v.Len() {
599602
if v.Kind() == reflect.Array {
600-
// Array. Zero the rest.
601-
z := reflect.Zero(v.Type().Elem())
603+
// Zero the remaining elements.
604+
zero := reflect.Zero(v.Type().Elem())
602605
for ; i < v.Len(); i++ {
603-
v.Index(i).Set(z)
606+
v.Index(i).Set(zero)
604607
}
605608
} else {
606609
v.SetLen(i)
607610
}
608611
}
609-
if i == 0 && v.Kind() == reflect.Slice {
612+
if v.Kind() == reflect.Slice && v.IsNil() {
613+
// Don't allow the resulting slice to be nil.
610614
v.Set(reflect.MakeSlice(v.Type(), 0, 0))
611615
}
612616
return nil

src/encoding/json/decode_test.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -2099,7 +2099,10 @@ func TestSkipArrayObjects(t *testing.T) {
20992099
// slices, and arrays.
21002100
// Issues 4900 and 8837, among others.
21012101
func TestPrefilled(t *testing.T) {
2102-
// Values here change, cannot reuse table across runs.
2102+
type T struct {
2103+
A, B int
2104+
}
2105+
// Values here change, cannot reuse the table across runs.
21032106
var prefillTests = []struct {
21042107
in string
21052108
ptr interface{}
@@ -2135,6 +2138,16 @@ func TestPrefilled(t *testing.T) {
21352138
ptr: &[...]int{1, 2},
21362139
out: &[...]int{3, 0},
21372140
},
2141+
{
2142+
in: `[{"A": 3}]`,
2143+
ptr: &[]T{{A: -1, B: -2}, {A: -3, B: -4}},
2144+
out: &[]T{{A: 3}},
2145+
},
2146+
{
2147+
in: `[{"A": 3}]`,
2148+
ptr: &[...]T{{A: -1, B: -2}, {A: -3, B: -4}},
2149+
out: &[...]T{{A: 3, B: -2}, {}},
2150+
},
21382151
}
21392152

21402153
for _, tt := range prefillTests {

0 commit comments

Comments
 (0)