Skip to content

Commit

Permalink
Improve encoding speed by caching types
Browse files Browse the repository at this point in the history
Comparison to last commit db2c063:
benchmark                                               old ns/op     new ns/op     delta
BenchmarkMarshal/Go_bool_to_CBOR_bool-2                 93.4          88.5          -5.25%
BenchmarkMarshal/Go_uint64_to_CBOR_positive_int-2       104           99.2          -4.62%
BenchmarkMarshal/Go_int64_to_CBOR_negative_int-2        97.1          92.7          -4.53%
BenchmarkMarshal/Go_float64_to_CBOR_float-2             102           97.2          -4.71%
BenchmarkMarshal/Go_[]uint8_to_CBOR_bytes-2             131           121           -7.63%
BenchmarkMarshal/Go_string_to_CBOR_text-2               122           121           -0.82%
BenchmarkMarshal/Go_[]int_to_CBOR_array-2               529           480           -9.26%
BenchmarkMarshal/Go_map[string]string_to_CBOR_map-2     2254          2194          -2.66
  • Loading branch information
fxamacker committed Nov 13, 2019
1 parent db2c063 commit 90423eb
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 104 deletions.
20 changes: 10 additions & 10 deletions BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ Benchmarks use data representing the following values:
* Array: `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]`
* Map: `{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"}}`

Benchmarks data show:
Go structs are faster than maps:
* decoding into struct is >66% faster than decoding into map.
* encoding struct is >63% faster than encoding map.
* encoding struct is >67% faster than encoding map.

Decoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
Expand All @@ -40,12 +40,12 @@ BenchmarkUnmarshal/CBOR_map_to_Go_struct-2 | 1324 ns/op| 224 B/op | 2 allocs/op

Encoding Benchmark | Time | Memory | Allocs
--- | ---: | ---: | ---:
BenchmarkMarshal/Go_bool_to_CBOR_bool-2 | 93.8 ns/op | 1 B/op | 1 allocs/op
BenchmarkMarshal/Go_uint64_to_CBOR_positive_int-2 | 103 ns/op | 16 B/op | 1 allocs/op
BenchmarkMarshal/Go_int64_to_CBOR_negative_int-2 | 96.9 ns/op | 3 B/op | 1 allocs/op
BenchmarkMarshal/Go_float64_to_CBOR_float-2 | 101 ns/op | 16 B/op | 1 allocs/op
BenchmarkMarshal/Go_[]uint8_to_CBOR_bytes-2 | 140 ns/op | 32 B/op | 1 allocs/op
BenchmarkMarshal/Go_bool_to_CBOR_bool-2 | 88.5 ns/op | 1 B/op | 1 allocs/op
BenchmarkMarshal/Go_uint64_to_CBOR_positive_int-2 | 99.2 ns/op | 16 B/op | 1 allocs/op
BenchmarkMarshal/Go_int64_to_CBOR_negative_int-2 | 92.7 ns/op | 3 B/op | 1 allocs/op
BenchmarkMarshal/Go_float64_to_CBOR_float-2 | 97.2 ns/op | 16 B/op | 1 allocs/op
BenchmarkMarshal/Go_[]uint8_to_CBOR_bytes-2 | 121 ns/op | 32 B/op | 1 allocs/op
BenchmarkMarshal/Go_string_to_CBOR_text-2 | 121 ns/op | 48 B/op | 1 allocs/op
BenchmarkMarshal/Go_[]int_to_CBOR_array-2 | 599 ns/op | 32 B/op | 1 allocs/op
BenchmarkMarshal/Go_map[string]string_to_CBOR_map-2 | 2211 ns/op | 576 B/op | 28 allocs/op
BenchmarkMarshal/Go_struct_to_CBOR_map-2 | 809 ns/op | 64 B/op | 1 allocs/op
BenchmarkMarshal/Go_[]int_to_CBOR_array-2 | 480 ns/op | 32 B/op | 1 allocs/op
BenchmarkMarshal/Go_map[string]string_to_CBOR_map-2 | 2194 ns/op | 576 B/op | 28 allocs/op
BenchmarkMarshal/Go_struct_to_CBOR_map-2 | 714 ns/op | 64 B/op | 1 allocs/op
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ err = enc.EndIndefinite()
## Benchmarks
Go structs are faster than maps:
* decoding into struct is >66% faster than decoding into map.
* encoding struct is >63% faster than encoding map.
* encoding struct is >67% faster than encoding map.

See [Benchmarks for fxamacker/cbor](BENCHMARKS.md).

Expand Down
36 changes: 36 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,42 @@ func BenchmarkMarshal(b *testing.B) {
}
}

func BenchmarkMarshalCanonical(b *testing.B) {
for _, bm := range []struct {
name string
cborData []byte
values []interface{}
}{
{"map", hexDecode("ad616161416162614261636143616461446165614561666146616761476168614861696149616a614a616c614c616d614d616e614e"), []interface{}{map[string]string{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"}, strc{A: "A", B: "B", C: "C", D: "D", E: "E", F: "F", G: "G", H: "H", I: "I", J: "J", L: "L", M: "M", N: "N"}}},
} {
for _, v := range bm.values {
name := "Go " + reflect.TypeOf(v).String() + " to CBOR " + bm.name
if reflect.TypeOf(v).Kind() == reflect.Struct {
name = "Go " + reflect.TypeOf(v).Kind().String() + " to CBOR " + bm.name
}
b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := cbor.Marshal(v, cbor.EncOptions{}); err != nil {
b.Fatal("Marshal:", err)
}
}
})
// Canonical encoding
name = "Go " + reflect.TypeOf(v).String() + " to CBOR " + bm.name + " canonical"
if reflect.TypeOf(v).Kind() == reflect.Struct {
name = "Go " + reflect.TypeOf(v).Kind().String() + " to CBOR " + bm.name + " canonical"
}
b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := cbor.Marshal(v, cbor.EncOptions{Canonical: true}); err != nil {
b.Fatal("Marshal:", err)
}
}
})
}
}
}

func BenchmarkEncode(b *testing.B) {
for _, bm := range encodeBenchmarks {
for _, v := range bm.values {
Expand Down
86 changes: 86 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package cbor

import (
"bytes"
"reflect"
"sort"
"sync"
)

type encodingStructType struct {
fields fields
canonicalFields fields
err error
}

// byCanonicalRule sorts fields by field name length and field name.
type byCanonicalRule struct {
fields
}

func (s byCanonicalRule) Less(i, j int) bool {
return bytes.Compare(s.fields[i].cborName, s.fields[j].cborName) <= 0
}

var (
decodingStructTypeCache sync.Map // map[reflect.Type]fields
encodingStructTypeCache sync.Map // map[reflect.Type]encodingStructType
encodingTypeCache sync.Map // map[reflect.Type]encodeFunc
)

func getDecodingStructType(t reflect.Type) fields {
if v, _ := decodingStructTypeCache.Load(t); v != nil {
return v.(fields)
}
flds := getFields(t)
for i := 0; i < len(flds); i++ {
flds[i].isUnmarshaler = implementsUnmarshaler(flds[i].typ)
}
decodingStructTypeCache.Store(t, flds)
return flds
}

func getEncodingStructType(t reflect.Type) encodingStructType {
if v, _ := encodingStructTypeCache.Load(t); v != nil {
return v.(encodingStructType)
}

flds := getFields(t)

var err error
es := getEncodeState()
for i := 0; i < len(flds); i++ {
ef := getEncodeFunc(flds[i].typ)
if ef == nil {
if err == nil {
err = &UnsupportedTypeError{t}
}
}
flds[i].ef = ef

encodeTypeAndAdditionalValue(es, byte(cborTypeTextString), uint64(len(flds[i].name)))
flds[i].cborName = make([]byte, es.Len()+len(flds[i].name))
copy(flds[i].cborName, es.Bytes())
copy(flds[i].cborName[es.Len():], flds[i].name)

es.Reset()
}
putEncodeState(es)

canonicalFields := make(fields, len(flds))
copy(canonicalFields, flds)
sort.Sort(byCanonicalRule{canonicalFields})

structType := encodingStructType{fields: flds, canonicalFields: canonicalFields, err: err}
encodingStructTypeCache.Store(t, structType)
return structType
}

func getEncodeFunc(t reflect.Type) encodeFunc {
if v, _ := encodingTypeCache.Load(t); v != nil {
return v.(encodeFunc)
}
f := getEncodeFuncInternal(t)
encodingTypeCache.Store(t, f)
return f
}
56 changes: 32 additions & 24 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,12 @@ func encodeStringInternal(e *encodeState, s string, opts EncOptions) (int, error
return n1 + n2, nil
}

func encodeArray(e *encodeState, v reflect.Value, opts EncOptions) (int, error) {
f := getEncodeFunc(v.Type().Elem())
if f == nil {
type arrayEncoder struct {
f encodeFunc
}

func (ae arrayEncoder) encodeArray(e *encodeState, v reflect.Value, opts EncOptions) (int, error) {
if ae.f == nil {
return 0, &UnsupportedTypeError{v.Type()}
}
if v.Kind() == reflect.Slice && v.IsNil() {
Expand All @@ -219,7 +222,7 @@ func encodeArray(e *encodeState, v reflect.Value, opts EncOptions) (int, error)
}
n := encodeTypeAndAdditionalValue(e, byte(cborTypeArray), uint64(len))
for i := 0; i < len; i++ {
n1, err := f(e, v.Index(i), opts)
n1, err := ae.f(e, v.Index(i), opts)
if err != nil {
return 0, err
}
Expand All @@ -228,13 +231,15 @@ func encodeArray(e *encodeState, v reflect.Value, opts EncOptions) (int, error)
return n, nil
}

func encodeMap(e *encodeState, v reflect.Value, opts EncOptions) (int, error) {
type mapEncoder struct {
kf, ef encodeFunc
}

func (me mapEncoder) encodeMap(e *encodeState, v reflect.Value, opts EncOptions) (int, error) {
if opts.Canonical {
return encodeMapCanonical(e, v, opts)
return me.encodeMapCanonical(e, v, opts)
}
kf := getEncodeFunc(v.Type().Key())
ef := getEncodeFunc(v.Type().Elem())
if kf == nil || ef == nil {
if me.kf == nil || me.ef == nil {
return 0, &UnsupportedTypeError{v.Type()}
}
if v.IsNil() {
Expand All @@ -247,11 +252,11 @@ func encodeMap(e *encodeState, v reflect.Value, opts EncOptions) (int, error) {
n := encodeTypeAndAdditionalValue(e, byte(cborTypeMap), uint64(len))
iter := v.MapRange()
for iter.Next() {
n1, err := kf(e, iter.Key(), opts)
n1, err := me.kf(e, iter.Key(), opts)
if err != nil {
return 0, err
}
n2, err := ef(e, iter.Value(), opts)
n2, err := me.ef(e, iter.Value(), opts)
if err != nil {
return 0, err
}
Expand Down Expand Up @@ -304,10 +309,8 @@ func putByCanonical(s *byCanonical) {
byCanonicalPool.Put(s)
}

func encodeMapCanonical(e *encodeState, v reflect.Value, opts EncOptions) (int, error) {
kf := getEncodeFunc(v.Type().Key())
ef := getEncodeFunc(v.Type().Elem())
if kf == nil || ef == nil {
func (me mapEncoder) encodeMapCanonical(e *encodeState, v reflect.Value, opts EncOptions) (int, error) {
if me.kf == nil || me.ef == nil {
return 0, &UnsupportedTypeError{v.Type()}
}
if v.IsNil() {
Expand All @@ -321,13 +324,13 @@ func encodeMapCanonical(e *encodeState, v reflect.Value, opts EncOptions) (int,

iter := v.MapRange()
for iter.Next() {
n1, err := kf(pairEncodeState, iter.Key(), opts)
n1, err := me.kf(pairEncodeState, iter.Key(), opts)
if err != nil {
putEncodeState(pairEncodeState)
putByCanonical(pairs)
return 0, err
}
n2, err := ef(pairEncodeState, iter.Value(), opts)
n2, err := me.ef(pairEncodeState, iter.Value(), opts)
if err != nil {
putEncodeState(pairEncodeState)
putByCanonical(pairs)
Expand Down Expand Up @@ -356,14 +359,19 @@ func encodeMapCanonical(e *encodeState, v reflect.Value, opts EncOptions) (int,
}

func encodeStruct(e *encodeState, v reflect.Value, opts EncOptions) (int, error) {
flds := getEncodingStructType(v.Type(), opts.Canonical)
structType := getEncodingStructType(v.Type())
if structType.err != nil {
return 0, structType.err
}

flds := structType.fields
if opts.Canonical {
flds = structType.canonicalFields
}

kve := getEncodeState() // encode key-value pairs based on struct field tag options
kvcount := 0
for i := 0; i < len(flds); i++ {
if flds[i].ef == nil {
return 0, &UnsupportedTypeError{v.Type()}
}
fv := v
ignore := false
for k, n := range flds[i].idx {
Expand Down Expand Up @@ -465,7 +473,7 @@ func getEncodeIndirectValueFunc(f encodeFunc) encodeFunc {
}
}

func getEncodeFunc(t reflect.Type) encodeFunc {
func getEncodeFuncInternal(t reflect.Type) encodeFunc {
if t.Kind() == reflect.Ptr {
for t.Kind() == reflect.Ptr {
t = t.Elem()
Expand Down Expand Up @@ -500,9 +508,9 @@ func getEncodeFunc(t reflect.Type) encodeFunc {
if t.Elem().Kind() == reflect.Uint8 {
return encodeByteString
}
return encodeArray
return arrayEncoder{f: getEncodeFunc(t.Elem())}.encodeArray
case reflect.Map:
return encodeMap
return mapEncoder{kf: getEncodeFunc(t.Key()), ef: getEncodeFunc(t.Elem())}.encodeMap
case reflect.Struct:
return encodeStruct
case reflect.Interface:
Expand Down
23 changes: 23 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1367,3 +1367,26 @@ func TestMarshalRawMessageValue(t *testing.T) {
}
}
}

func TestCyclicDataStructure(t *testing.T) {
type Node struct {
V int `cbor:"v"`
N *Node `cbor:"n,omitempty"`
}
v := Node{1, &Node{2, &Node{3, nil}}} // linked list: 1, 2, 3
wantCborData := []byte{0xa2, 0x61, 0x76, 0x01, 0x61, 0x6e, 0xa2, 0x61, 0x76, 0x02, 0x61, 0x6e, 0xa1, 0x61, 0x76, 0x03} // {v: 1, n: {v: 2, n: {v: 3}}}
cborData, err := cbor.Marshal(v, cbor.EncOptions{})
if err != nil {
t.Fatalf("Marshal(%v) returns error %s", v, err)
}
if !bytes.Equal(wantCborData, cborData) {
t.Errorf("Marshal(%v) = 0x%0x, want 0x%0x", v, cborData, wantCborData)
}
var v1 Node
if err = cbor.Unmarshal(cborData, &v1); err != nil {
t.Fatalf("Unmarshal(0x%0x) returns error %s", cborData, err)
}
if !reflect.DeepEqual(v, v1) {
t.Errorf("Unmarshal(0x%0x) returns %+v, want %+v", cborData, v1, v)
}
}
Loading

0 comments on commit 90423eb

Please sign in to comment.