Skip to content

Commit

Permalink
encoding/json: index names for the struct decoder
Browse files Browse the repository at this point in the history
In the common case, structs have a handful of fields and most inputs
match struct field names exactly.

The previous code would do a linear search over the fields, stopping at
the first exact match, and otherwise using the first case insensitive
match.

This is unfortunate, because it means that for the common case, we'd do
a linear search with bytes.Equal. Even for structs with only two or
three fields, that is pretty wasteful.

Worse even, up until the exact match was found via the linear search,
all previous fields would run their equalFold functions, which aren't
cheap even in the simple case.

Instead, cache a map along with the field list that indexes the fields
by their name. This way, a case sensitive field search doesn't involve a
linear search, nor does it involve any equalFold func calls.

This patch should also slightly speed up cases where there's a case
insensitive match but not a case sensitive one, as then we'd avoid
calling bytes.Equal on all the fields. Though that's not a common case,
and there are no benchmarks for it.

name           old time/op    new time/op    delta
CodeDecoder-8    11.0ms ± 0%    10.6ms ± 1%  -4.42%  (p=0.000 n=9+10)

name           old speed      new speed      delta
CodeDecoder-8   176MB/s ± 0%   184MB/s ± 1%  +4.62%  (p=0.000 n=9+10)

name           old alloc/op   new alloc/op   delta
CodeDecoder-8    2.28MB ± 0%    2.28MB ± 0%    ~     (p=0.725 n=10+10)

name           old allocs/op  new allocs/op  delta
CodeDecoder-8     76.9k ± 0%     76.9k ± 0%    ~     (all equal)

Updates #28923.

Change-Id: I9929c1f06c76505e5b96914199315dbdaae5dc76
Reviewed-on: https://go-review.googlesource.com/c/go/+/172918
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
  • Loading branch information
mvdan committed Apr 23, 2019
1 parent 5a51400 commit e1fc8f3
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 19 deletions.
23 changes: 13 additions & 10 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
package json

import (
"bytes"
"encoding"
"encoding/base64"
"fmt"
Expand Down Expand Up @@ -691,7 +690,7 @@ func (d *decodeState) object(v reflect.Value) error {
return nil
}

var fields []field
var fields structFields

// Check type of target:
// struct or
Expand Down Expand Up @@ -761,14 +760,18 @@ func (d *decodeState) object(v reflect.Value) error {
subv = mapElem
} else {
var f *field
for i := range fields {
ff := &fields[i]
if bytes.Equal(ff.nameBytes, key) {
f = ff
break
}
if f == nil && ff.equalFold(ff.nameBytes, key) {
f = ff
if i, ok := fields.nameIndex[string(key)]; ok {
// Found an exact name match.
f = &fields.list[i]
} else {
// Fall back to the expensive case-insensitive
// linear search.
for i := range fields.list {
ff := &fields.list[i]
if ff.equalFold(ff.nameBytes, key) {
f = ff
break
}
}
}
if f != nil {
Expand Down
27 changes: 18 additions & 9 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,14 +621,19 @@ func unsupportedTypeEncoder(e *encodeState, v reflect.Value, _ encOpts) {
}

type structEncoder struct {
fields []field
fields structFields
}

type structFields struct {
list []field
nameIndex map[string]int
}

func (se structEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
next := byte('{')
FieldLoop:
for i := range se.fields {
f := &se.fields[i]
for i := range se.fields.list {
f := &se.fields.list[i]

// Find the nested struct field by following f.index.
fv := v
Expand Down Expand Up @@ -1063,7 +1068,7 @@ func (x byIndex) Less(i, j int) bool {
// typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs.
func typeFields(t reflect.Type) []field {
func typeFields(t reflect.Type) structFields {
// Anonymous fields to explore at the current level and the next.
current := []field{}
next := []field{{typ: t}}
Expand Down Expand Up @@ -1237,7 +1242,11 @@ func typeFields(t reflect.Type) []field {
f := &fields[i]
f.encoder = typeEncoder(typeByIndex(t, f.index))
}
return fields
nameIndex := make(map[string]int, len(fields))
for i, field := range fields {
nameIndex[field.name] = i
}
return structFields{fields, nameIndex}
}

// dominantField looks through the fields, all of which are known to
Expand All @@ -1256,13 +1265,13 @@ func dominantField(fields []field) (field, bool) {
return fields[0], true
}

var fieldCache sync.Map // map[reflect.Type][]field
var fieldCache sync.Map // map[reflect.Type]structFields

// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
func cachedTypeFields(t reflect.Type) structFields {
if f, ok := fieldCache.Load(t); ok {
return f.([]field)
return f.(structFields)
}
f, _ := fieldCache.LoadOrStore(t, typeFields(t))
return f.([]field)
return f.(structFields)
}

0 comments on commit e1fc8f3

Please sign in to comment.