Skip to content

Commit df68afd

Browse files
pascaldekloebradfitz
authored andcommitted
encoding/json: reduce unmarshal mallocs for unmapped fields
JSON decoding performs poorly for unmapped and ignored fields. We noticed better performance when unmarshalling unused fields. The loss comes mostly from calls to scanner.error as described at #17914. benchmark old ns/op new ns/op delta BenchmarkIssue10335-8 431 408 -5.34% BenchmarkUnmapped-8 1744 1314 -24.66% benchmark old allocs new allocs delta BenchmarkIssue10335-8 4 3 -25.00% BenchmarkUnmapped-8 18 4 -77.78% benchmark old bytes new bytes delta BenchmarkIssue10335-8 320 312 -2.50% BenchmarkUnmapped-8 568 344 -39.44% Fixes #17914, improves #10335 Change-Id: I7d4258a94eb287c0fe49e7334795209b90434cd0 Reviewed-on: https://go-review.googlesource.com/33276 Reviewed-by: Brad Fitzpatrick <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 7bb5b2d commit df68afd

File tree

2 files changed

+39
-34
lines changed

2 files changed

+39
-34
lines changed

src/encoding/json/bench_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,14 @@ func BenchmarkIssue10335(b *testing.B) {
221221
}
222222
}
223223
}
224+
225+
func BenchmarkUnmapped(b *testing.B) {
226+
b.ReportAllocs()
227+
var s struct{}
228+
j := []byte(`{"s": "hello", "y": 2, "o": {"x": 0}, "a": [1, 99, {"x": 1}]}`)
229+
for n := 0; n < b.N; n++ {
230+
if err := Unmarshal(j, &s); err != nil {
231+
b.Fatal(err)
232+
}
233+
}
234+
}

src/encoding/json/decode.go

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -359,47 +359,40 @@ func (d *decodeState) scanWhile(op int) int {
359359
return newOp
360360
}
361361

362+
// discardObject and discardArray are dummy data targets
363+
// used by the (*decodeState).value method, which
364+
// accepts a zero reflect.Value to discard a value.
365+
// The (*decodeState).object and (*decodeState).array methods,
366+
// however, require a valid reflect.Value destination.
367+
// These are the target values used when the caller of value
368+
// wants to skip a field.
369+
//
370+
// Because these values refer to zero-sized objects
371+
// and thus can't be mutated, they're safe for concurrent use
372+
// by different goroutines unmarshalling skipped fields.
373+
var (
374+
discardObject = reflect.ValueOf(struct{}{})
375+
discardArray = reflect.ValueOf([0]interface{}{})
376+
)
377+
362378
// value decodes a JSON value from d.data[d.off:] into the value.
363-
// it updates d.off to point past the decoded value.
379+
// It updates d.off to point past the decoded value. If v is
380+
// invalid, the JSON value is discarded.
364381
func (d *decodeState) value(v reflect.Value) {
365-
if !v.IsValid() {
366-
_, rest, err := nextValue(d.data[d.off:], &d.nextscan)
367-
if err != nil {
368-
d.error(err)
369-
}
370-
d.off = len(d.data) - len(rest)
371-
372-
// d.scan thinks we're still at the beginning of the item.
373-
// Feed in an empty string - the shortest, simplest value -
374-
// so that it knows we got to the end of the value.
375-
if d.scan.redo {
376-
// rewind.
377-
d.scan.redo = false
378-
d.scan.step = stateBeginValue
379-
}
380-
d.scan.step(&d.scan, '"')
381-
d.scan.step(&d.scan, '"')
382-
383-
n := len(d.scan.parseState)
384-
if n > 0 && d.scan.parseState[n-1] == parseObjectKey {
385-
// d.scan thinks we just read an object key; finish the object
386-
d.scan.step(&d.scan, ':')
387-
d.scan.step(&d.scan, '"')
388-
d.scan.step(&d.scan, '"')
389-
d.scan.step(&d.scan, '}')
390-
}
391-
392-
return
393-
}
394-
395382
switch op := d.scanWhile(scanSkipSpace); op {
396383
default:
397384
d.error(errPhase)
398385

399386
case scanBeginArray:
387+
if !v.IsValid() {
388+
v = discardArray
389+
}
400390
d.array(v)
401391

402392
case scanBeginObject:
393+
if !v.IsValid() {
394+
v = discardObject
395+
}
403396
d.object(v)
404397

405398
case scanBeginLiteral:
@@ -517,8 +510,7 @@ func (d *decodeState) array(v reflect.Value) {
517510
d.off--
518511
d.next()
519512
return
520-
case reflect.Array:
521-
case reflect.Slice:
513+
case reflect.Array, reflect.Slice:
522514
break
523515
}
524516

@@ -797,7 +789,9 @@ func (d *decodeState) literal(v reflect.Value) {
797789
d.off--
798790
d.scan.undo(op)
799791

800-
d.literalStore(d.data[start:d.off], v, false)
792+
if v.IsValid() {
793+
d.literalStore(d.data[start:d.off], v, false)
794+
}
801795
}
802796

803797
// convertNumber converts the number literal s to a float64 or a Number

0 commit comments

Comments
 (0)