From e79221c6f6fb924b9fa9cb374c5875994be0ced6 Mon Sep 17 00:00:00 2001 From: Phil Pearl Date: Mon, 14 Feb 2022 11:09:22 +0000 Subject: [PATCH] Unmarshalling float with lots of digits can cause >= 2 allocations per number We can fix this with a little bit of scratch space. --- benchmarks/float_test.go | 28 +++++++++++++++++++++++++ benchmarks/stream_test.go | 7 ++++--- iter.go | 7 +++++-- iter_float.go | 43 +++++++++++++++++++++++++++++++++------ 4 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 benchmarks/float_test.go diff --git a/benchmarks/float_test.go b/benchmarks/float_test.go new file mode 100644 index 00000000..30e88527 --- /dev/null +++ b/benchmarks/float_test.go @@ -0,0 +1,28 @@ +package test + +import ( + "testing" + + jsoniter "github.com/json-iterator/go" +) + +func BenchmarkFloatUnmarshal(b *testing.B) { + type floaty struct { + A float32 + B float64 + } + + data, err := jsoniter.Marshal(floaty{A: 1.111111111111111, B: 1.11111111111111}) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + b.ReportAllocs() + var out floaty + for i := 0; i < b.N; i++ { + if err := jsoniter.Unmarshal(data, &out); err != nil { + b.Fatal(err) + } + } +} diff --git a/benchmarks/stream_test.go b/benchmarks/stream_test.go index ea413a1e..c6e6283f 100644 --- a/benchmarks/stream_test.go +++ b/benchmarks/stream_test.go @@ -10,7 +10,8 @@ import ( func Benchmark_stream_encode_big_object(b *testing.B) { var buf bytes.Buffer - var stream = jsoniter.NewStream(jsoniter.ConfigDefault, &buf, 100) + stream := jsoniter.NewStream(jsoniter.ConfigDefault, &buf, 100) + b.ReportAllocs() for i := 0; i < b.N; i++ { buf.Reset() stream.Reset(&buf) @@ -22,13 +23,13 @@ func Benchmark_stream_encode_big_object(b *testing.B) { } func TestEncodeObject(t *testing.T) { - var stream = jsoniter.NewStream(jsoniter.ConfigDefault, nil, 100) + stream := jsoniter.NewStream(jsoniter.ConfigDefault, nil, 100) encodeObject(stream) if stream.Error != nil { t.Errorf("error encoding a test object: %+v", stream.Error) return } - var m = make(map[string]interface{}) + m := make(map[string]interface{}) if err := jsoniter.Unmarshal(stream.Buffer(), &m); err != nil { t.Errorf("error unmarshaling a test object: %+v", err) return diff --git a/iter.go b/iter.go index 29b31cf7..e8e7f1a0 100644 --- a/iter.go +++ b/iter.go @@ -26,8 +26,10 @@ const ( ObjectValue ) -var hexDigits []byte -var valueTypes []ValueType +var ( + hexDigits []byte + valueTypes []ValueType +) func init() { hexDigits = make([]byte, 256) @@ -78,6 +80,7 @@ type Iterator struct { captureStartedAt int captured []byte Error error + scratch []byte Attachment interface{} // open for customized decoder } diff --git a/iter_float.go b/iter_float.go index 8a3d8b6f..9af2eeff 100644 --- a/iter_float.go +++ b/iter_float.go @@ -11,9 +11,11 @@ import ( var floatDigits []int8 -const invalidCharForNumber = int8(-1) -const endOfNumber = int8(-2) -const dotInNumber = int8(-3) +const ( + invalidCharForNumber = int8(-1) + endOfNumber = int8(-2) + dotInNumber = int8(-3) +) func init() { floatDigits = make([]int8, 256) @@ -66,7 +68,7 @@ func (iter *Iterator) ReadBigInt() (ret *big.Int) { return ret } -//ReadFloat32 read float32 +// ReadFloat32 read float32 func (iter *Iterator) ReadFloat32() (ret float32) { c := iter.nextToken() if c == '-' { @@ -185,11 +187,39 @@ load_loop: return *(*string)(unsafe.Pointer(&str)) } +func (iter *Iterator) appendNumber(buf []byte) []byte { +load_loop: + for { + for i := iter.head; i < iter.tail; i++ { + c := iter.buf[i] + switch c { + case '+', '-', '.', 'e', 'E', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + buf = append(buf, c) + continue + default: + iter.head = i + break load_loop + } + } + if !iter.loadMore() { + break + } + } + if iter.Error != nil && iter.Error != io.EOF { + return buf + } + if len(buf) == 0 { + iter.ReportError("readNumberAsString", "invalid number") + } + return buf +} + func (iter *Iterator) readFloat32SlowPath() (ret float32) { - str := iter.readNumberAsString() + iter.scratch = iter.appendNumber(iter.scratch[:0]) if iter.Error != nil && iter.Error != io.EOF { return } + str := *(*string)(unsafe.Pointer(&iter.scratch)) errMsg := validateFloat(str) if errMsg != "" { iter.ReportError("readFloat32SlowPath", errMsg) @@ -297,10 +327,11 @@ non_decimal_loop: } func (iter *Iterator) readFloat64SlowPath() (ret float64) { - str := iter.readNumberAsString() + iter.scratch = iter.appendNumber(iter.scratch[:0]) if iter.Error != nil && iter.Error != io.EOF { return } + str := *(*string)(unsafe.Pointer(&iter.scratch)) errMsg := validateFloat(str) if errMsg != "" { iter.ReportError("readFloat64SlowPath", errMsg)