gojay

high performance JSON encoder/decoder with stream API for Golang
git clone git://git.lair.cx/gojay
Log | Files | Refs | README | LICENSE

commit 7ccc975d96d5fddcb7dc679fcc867b55c1bbf079
parent 4ddbe4cccf1017685edfbaf701f8248d743597e8
Author: francoispqt <francois@parquet.ninja>
Date:   Sat,  5 May 2018 01:59:17 +0800

add buffr growing optimizations

Diffstat:
Mbenchmarks/encoder/encoder_bench_small_test.go | 19+++++++++++++++++++
Mencode_array.go | 12+++++++++---
Mencode_bool.go | 9+++++++--
Mencode_builder.go | 6------
Mencode_builder_test.go | 16----------------
Mencode_number.go | 23+++++++++++++++++------
Mencode_object.go | 44++++++++++++++++++++++++++++++++++++--------
Mencode_object_test.go | 12++++++++++++
Mencode_string.go | 7+++++--
Mencode_string_test.go | 4++--
10 files changed, 107 insertions(+), 45 deletions(-)

diff --git a/benchmarks/encoder/encoder_bench_small_test.go b/benchmarks/encoder/encoder_bench_small_test.go @@ -48,6 +48,25 @@ func BenchmarkGoJayEncodeSmallStruct(b *testing.B) { } } +func BenchmarkGoJayEncodeSmallFunc(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := gojay.MarshalObject(gojay.EncodeObjectFunc(func(enc *gojay.Encoder) { + enc.AddIntKey("st", 1) + enc.AddIntKey("sid", 1) + enc.AddStringKey("tt", "test") + enc.AddIntKey("gr", 1) + enc.AddStringKey("uuid", "test") + enc.AddStringKey("ip", "test") + enc.AddStringKey("ua", "test") + enc.AddIntKey("tz", 1) + enc.AddIntKey("v", 1) + })); err != nil { + b.Fatal(err) + } + } +} + func TestGoJayEncodeSmallStruct(t *testing.T) { if output, err := gojay.MarshalObject(benchmarks.NewSmallPayload()); err != nil { t.Fatal(err) diff --git a/encode_array.go b/encode_array.go @@ -25,6 +25,7 @@ func (enc *Encoder) encodeArray(v MarshalerArray) ([]byte, error) { // value must implement Marshaler func (enc *Encoder) AddArray(v MarshalerArray) { if v.IsNil() { + enc.grow(3) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -33,6 +34,7 @@ func (enc *Encoder) AddArray(v MarshalerArray) { enc.writeByte(']') return } + enc.grow(100) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -48,6 +50,7 @@ func (enc *Encoder) AddArrayOmitEmpty(v MarshalerArray) { if v.IsNil() { return } + enc.grow(4) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -61,22 +64,24 @@ func (enc *Encoder) AddArrayOmitEmpty(v MarshalerArray) { // value must implement Marshaler func (enc *Encoder) AddArrayKey(key string, v MarshalerArray) { if v.IsNil() { + enc.grow(2 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKeyArr) enc.writeByte(']') return } + enc.grow(5 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '[' && r != '{' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKeyArr) v.MarshalArray(enc) enc.writeByte(']') @@ -88,12 +93,13 @@ func (enc *Encoder) AddArrayKeyOmitEmpty(key string, v MarshalerArray) { if v.IsNil() { return } + enc.grow(5 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '[' && r != '{' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKeyArr) v.MarshalArray(enc) enc.writeByte(']') diff --git a/encode_bool.go b/encode_bool.go @@ -18,6 +18,7 @@ func (enc *Encoder) EncodeBool(v bool) error { // encodeBool encodes a bool to JSON func (enc *Encoder) encodeBool(v bool) ([]byte, error) { + enc.grow(5) if v { enc.writeString("true") } else { @@ -28,6 +29,7 @@ func (enc *Encoder) encodeBool(v bool) ([]byte, error) { // AddBool adds a bool to be encoded, must be used inside a slice or array encoding (does not encode a key) func (enc *Encoder) AddBool(v bool) { + enc.grow(5) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -44,6 +46,7 @@ func (enc *Encoder) AddBoolOmitEmpty(v bool) { if v == false { return } + enc.grow(5) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -53,12 +56,13 @@ func (enc *Encoder) AddBoolOmitEmpty(v bool) { // AddBoolKey adds a bool to be encoded, must be used inside an object as it will encode a key. func (enc *Encoder) AddBoolKey(key string, value bool) { + enc.grow(5 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKey) enc.buf = strconv.AppendBool(enc.buf, value) } @@ -69,12 +73,13 @@ func (enc *Encoder) AddBoolKeyOmitEmpty(key string, v bool) { if v == false { return } + enc.grow(5 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKey) enc.buf = strconv.AppendBool(enc.buf, v) } diff --git a/encode_builder.go b/encode_builder.go @@ -1,15 +1,9 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. package gojay // grow grows b's capacity, if necessary, to guarantee space for // another n bytes. After grow(n), at least n bytes can be written to b // without another allocation. If n is negative, grow panics. func (enc *Encoder) grow(n int) { - if n < 0 { - panic("Builder.grow: negative count") - } if cap(enc.buf)-len(enc.buf) < n { Buf := make([]byte, len(enc.buf), 2*cap(enc.buf)+n) copy(Buf, enc.buf) diff --git a/encode_builder_test.go b/encode_builder_test.go @@ -1,17 +1 @@ package gojay - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEncoderBuilderError(t *testing.T) { - enc := NewEncoder(nil) - defer func() { - err := recover() - assert.NotNil(t, err, "err is not nil as we pass an invalid number to grow") - }() - enc.grow(-1) - assert.True(t, false, "should not be called") -} diff --git a/encode_number.go b/encode_number.go @@ -79,6 +79,7 @@ func (enc *Encoder) encodeFloat32(n float32) ([]byte, error) { // AddInt adds an int to be encoded, must be used inside a slice or array encoding (does not encode a key) func (enc *Encoder) AddInt(v int) { + enc.grow(10) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -92,6 +93,7 @@ func (enc *Encoder) AddIntOmitEmpty(v int) { if v == 0 { return } + enc.grow(10) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -101,6 +103,7 @@ func (enc *Encoder) AddIntOmitEmpty(v int) { // AddFloat adds a float64 to be encoded, must be used inside a slice or array encoding (does not encode a key) func (enc *Encoder) AddFloat(v float64) { + enc.grow(10) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -114,6 +117,7 @@ func (enc *Encoder) AddFloatOmitEmpty(v float64) { if v == 0 { return } + enc.grow(10) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -136,6 +140,7 @@ func (enc *Encoder) AddFloat32OmitEmpty(v float32) { if v == 0 { return } + enc.grow(10) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -145,12 +150,13 @@ func (enc *Encoder) AddFloat32OmitEmpty(v float32) { // AddIntKey adds an int to be encoded, must be used inside an object as it will encode a key func (enc *Encoder) AddIntKey(key string, v int) { + enc.grow(10 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKey) enc.buf = strconv.AppendInt(enc.buf, int64(v), 10) } @@ -161,12 +167,13 @@ func (enc *Encoder) AddIntKeyOmitEmpty(key string, v int) { if v == 0 { return } + enc.grow(10 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKey) enc.buf = strconv.AppendInt(enc.buf, int64(v), 10) } @@ -177,8 +184,9 @@ func (enc *Encoder) AddFloatKey(key string, value float64) { if ok && r != '{' && r != '[' { enc.writeByte(',') } + enc.grow(10) enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKey) enc.buf = strconv.AppendFloat(enc.buf, value, 'f', -1, 64) } @@ -189,24 +197,26 @@ func (enc *Encoder) AddFloatKeyOmitEmpty(key string, v float64) { if v == 0 { return } + enc.grow(10 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKey) enc.buf = strconv.AppendFloat(enc.buf, v, 'f', -1, 64) } // AddFloat32Key adds a float32 to be encoded, must be used inside an object as it will encode a key func (enc *Encoder) AddFloat32Key(key string, v float32) { + enc.grow(10 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeByte('"') enc.writeByte(':') enc.buf = strconv.AppendFloat(enc.buf, float64(v), 'f', -1, 32) @@ -218,12 +228,13 @@ func (enc *Encoder) AddFloat32KeyOmitEmpty(key string, v float32) { if v == 0 { return } + enc.grow(10 + len(key)) r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKey) enc.buf = strconv.AppendFloat(enc.buf, float64(v), 'f', -1, 32) } diff --git a/encode_object.go b/encode_object.go @@ -22,10 +22,13 @@ func (enc *Encoder) EncodeObject(v MarshalerObject) error { } return nil } + func (enc *Encoder) encodeObject(v MarshalerObject) ([]byte, error) { - enc.grow(200) + enc.grow(500) enc.writeByte('{') - v.MarshalObject(enc) + if !v.IsNil() { + v.MarshalObject(enc) + } enc.writeByte('}') return enc.buf, enc.err } @@ -34,6 +37,7 @@ func (enc *Encoder) encodeObject(v MarshalerObject) ([]byte, error) { // value must implement MarshalerObject func (enc *Encoder) AddObject(v MarshalerObject) { if v.IsNil() { + enc.grow(2) r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') @@ -42,6 +46,7 @@ func (enc *Encoder) AddObject(v MarshalerObject) { enc.writeByte('}') return } + enc.grow(4) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -58,6 +63,7 @@ func (enc *Encoder) AddObjectOmitEmpty(v MarshalerObject) { if v.IsNil() { return } + enc.grow(2) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -71,22 +77,24 @@ func (enc *Encoder) AddObjectOmitEmpty(v MarshalerObject) { // value must implement MarshalerObject func (enc *Encoder) AddObjectKey(key string, value MarshalerObject) { if value.IsNil() { + enc.grow(2 + len(key)) r, ok := enc.getPreviousRune() - if ok && r != '{' && r != '[' { + if ok && r != '{' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKeyObj) enc.writeByte('}') return } + enc.grow(5 + len(key)) r, ok := enc.getPreviousRune() - if ok && r != '{' && r != '[' { + if ok && r != '{' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKeyObj) value.MarshalObject(enc) enc.writeByte('}') @@ -99,13 +107,33 @@ func (enc *Encoder) AddObjectKeyOmitEmpty(key string, value MarshalerObject) { if value.IsNil() { return } + enc.grow(5 + len(key)) r, ok := enc.getPreviousRune() - if ok && r != '{' && r != '[' { + if ok && r != '{' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(key) + enc.writeStringEscape(key) enc.writeBytes(objKeyObj) value.MarshalObject(enc) enc.writeByte('}') } + +// EncodeObjectFunc is a custom func type implementating MarshaleObject. +// Use it to cast a func(*Encoder) to Marshal and object. +// +// enc := gojay.NewEncoder(io.Writer) +// enc.EncoderObject(gojay.EncodeObjectFunc(func(enc *gojay.Encoder) { +// enc.AddStringKey("hello", "world") +// })) +type EncodeObjectFunc func(*Encoder) + +// MarshalObject implements MarshalerObject. +func (f EncodeObjectFunc) MarshalObject(enc *Encoder) { + f(enc) +} + +// IsNil implements MarshalerObject. +func (f EncodeObjectFunc) IsNil() bool { + return f == nil +} diff --git a/encode_object_test.go b/encode_object_test.go @@ -298,6 +298,18 @@ func TestEncoderObjectMarshalAPI(t *testing.T) { string(r), "Result of marshalling is different as the one expected") }) + t.Run("marshal-object-func", func(t *testing.T) { + f := EncodeObjectFunc(func(enc *Encoder) { + enc.AddStringKey("test", "test") + }) + r, err := Marshal(f) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"test":"test"}`, + string(r), + "Result of marshalling is different as the one expected") + }) } type TestObectOmitEmpty struct { diff --git a/encode_string.go b/encode_string.go @@ -24,6 +24,7 @@ func (enc *Encoder) encodeString(v string) ([]byte, error) { // AddString adds a string to be encoded, must be used inside a slice or array encoding (does not encode a key) func (enc *Encoder) AddString(v string) { + enc.grow(len(v) + 4) r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') @@ -50,8 +51,9 @@ func (enc *Encoder) AddStringOmitEmpty(v string) { // AddStringKey adds a string to be encoded, must be used inside an object as it will encode a key func (enc *Encoder) AddStringKey(key, v string) { + enc.grow(len(key) + len(v) + 5) r, ok := enc.getPreviousRune() - if ok && r != '{' && r != '[' { + if ok && r != '{' { enc.writeByte(',') } enc.writeByte('"') @@ -67,8 +69,9 @@ func (enc *Encoder) AddStringKeyOmitEmpty(key, v string) { if v == "" { return } + enc.grow(len(key) + len(v) + 5) r, ok := enc.getPreviousRune() - if ok && r != '{' && r != '[' { + if ok && r != '{' { enc.writeByte(',') } enc.writeByte('"') diff --git a/encode_string_test.go b/encode_string_test.go @@ -69,14 +69,14 @@ func TestEncoderStringEncodeAPI(t *testing.T) { "Result of marshalling is different as the one expected") }) t.Run("escaped-sequence3", func(t *testing.T) { - str := "hello \r world" + str := "hello \r world 𝄞" builder := &strings.Builder{} enc := NewEncoder(builder) err := enc.EncodeString(str) assert.Nil(t, err, "Error should be nil") assert.Equal( t, - `"hello \r world"`, + `"hello \r world 𝄞"`, builder.String(), "Result of marshalling is different as the one expected") })