Commit 22d37707 authored by Phil Pearl's avatar Phil Pearl Committed by Daniel Martí

encoding/json: improve performance of Compact

This change improves performance of Compact by using a sync.Pool to allow re-use
of a scanner. This also has the side-effect of removing an allocation for each
field that implements Marshaler when marshalling JSON.

name               old time/op    new time/op    delta
EncodeMarshaler-8     118ns ± 2%     104ns ± 1%  -12.21%  (p=0.001 n=7+7)

name               old alloc/op   new alloc/op   delta
EncodeMarshaler-8      100B ± 0%       36B ± 0%  -64.00%  (p=0.000 n=8+8)

name               old allocs/op  new allocs/op  delta
EncodeMarshaler-8      3.00 ± 0%      2.00 ± 0%  -33.33%  (p=0.000 n=8+8)

Change-Id: Ic70c61a0a6354823da5220f5aad04b94c054f233
Reviewed-on: https://go-review.googlesource.com/c/go/+/200864Reviewed-by: default avatarDaniel Martí <mvdan@mvdan.cc>
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 31bfab4a
...@@ -389,3 +389,22 @@ func BenchmarkTypeFieldsCache(b *testing.B) { ...@@ -389,3 +389,22 @@ func BenchmarkTypeFieldsCache(b *testing.B) {
}) })
} }
} }
func BenchmarkEncodeMarshaler(b *testing.B) {
b.ReportAllocs()
m := struct {
A int
B RawMessage
}{}
b.RunParallel(func(pb *testing.PB) {
enc := NewEncoder(ioutil.Discard)
for pb.Next() {
if err := enc.Encode(&m); err != nil {
b.Fatal("Encode:", err)
}
}
})
}
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
package json package json
import "bytes" import (
"bytes"
)
// Compact appends to dst the JSON-encoded src with // Compact appends to dst the JSON-encoded src with
// insignificant space characters elided. // insignificant space characters elided.
...@@ -14,8 +16,8 @@ func Compact(dst *bytes.Buffer, src []byte) error { ...@@ -14,8 +16,8 @@ func Compact(dst *bytes.Buffer, src []byte) error {
func compact(dst *bytes.Buffer, src []byte, escape bool) error { func compact(dst *bytes.Buffer, src []byte, escape bool) error {
origLen := dst.Len() origLen := dst.Len()
var scan scanner scan := newScanner()
scan.reset() defer freeScanner(scan)
start := 0 start := 0
for i, c := range src { for i, c := range src {
if escape && (c == '<' || c == '>' || c == '&') { if escape && (c == '<' || c == '>' || c == '&') {
...@@ -36,7 +38,7 @@ func compact(dst *bytes.Buffer, src []byte, escape bool) error { ...@@ -36,7 +38,7 @@ func compact(dst *bytes.Buffer, src []byte, escape bool) error {
dst.WriteByte(hex[src[i+2]&0xF]) dst.WriteByte(hex[src[i+2]&0xF])
start = i + 3 start = i + 3
} }
v := scan.step(&scan, c) v := scan.step(scan, c)
if v >= scanSkipSpace { if v >= scanSkipSpace {
if v == scanError { if v == scanError {
break break
...@@ -78,13 +80,13 @@ func newline(dst *bytes.Buffer, prefix, indent string, depth int) { ...@@ -78,13 +80,13 @@ func newline(dst *bytes.Buffer, prefix, indent string, depth int) {
// if src ends in a trailing newline, so will dst. // if src ends in a trailing newline, so will dst.
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
origLen := dst.Len() origLen := dst.Len()
var scan scanner scan := newScanner()
scan.reset() defer freeScanner(scan)
needIndent := false needIndent := false
depth := 0 depth := 0
for _, c := range src { for _, c := range src {
scan.bytes++ scan.bytes++
v := scan.step(&scan, c) v := scan.step(scan, c)
if v == scanSkipSpace { if v == scanSkipSpace {
continue continue
} }
......
...@@ -13,11 +13,16 @@ package json ...@@ -13,11 +13,16 @@ package json
// This file starts with two simple examples using the scanner // This file starts with two simple examples using the scanner
// before diving into the scanner itself. // before diving into the scanner itself.
import "strconv" import (
"strconv"
"sync"
)
// Valid reports whether data is a valid JSON encoding. // Valid reports whether data is a valid JSON encoding.
func Valid(data []byte) bool { func Valid(data []byte) bool {
return checkValid(data, &scanner{}) == nil scan := newScanner()
defer freeScanner(scan)
return checkValid(data, scan) == nil
} }
// checkValid verifies that data is valid JSON-encoded data. // checkValid verifies that data is valid JSON-encoded data.
...@@ -45,7 +50,7 @@ type SyntaxError struct { ...@@ -45,7 +50,7 @@ type SyntaxError struct {
func (e *SyntaxError) Error() string { return e.msg } func (e *SyntaxError) Error() string { return e.msg }
// A scanner is a JSON scanning state machine. // A scanner is a JSON scanning state machine.
// Callers call scan.reset() and then pass bytes in one at a time // Callers call scan.reset and then pass bytes in one at a time
// by calling scan.step(&scan, c) for each byte. // by calling scan.step(&scan, c) for each byte.
// The return value, referred to as an opcode, tells the // The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning // caller about significant parsing events like beginning
...@@ -72,10 +77,33 @@ type scanner struct { ...@@ -72,10 +77,33 @@ type scanner struct {
// Error that happened, if any. // Error that happened, if any.
err error err error
// total bytes consumed, updated by decoder.Decode // total bytes consumed, updated by decoder.Decode (and deliberately
// not set to zero by scan.reset)
bytes int64 bytes int64
} }
var scannerPool = sync.Pool{
New: func() interface{} {
return &scanner{}
},
}
func newScanner() *scanner {
scan := scannerPool.Get().(*scanner)
// scan.reset by design doesn't set bytes to zero
scan.bytes = 0
scan.reset()
return scan
}
func freeScanner(scan *scanner) {
// Avoid hanging on to too much memory in extreme cases.
if len(scan.parseState) > 1024 {
scan.parseState = nil
}
scannerPool.Put(scan)
}
// These values are returned by the state transition functions // These values are returned by the state transition functions
// assigned to scanner.state and the method scanner.eof. // assigned to scanner.state and the method scanner.eof.
// They give details about the current state of the scan that // They give details about the current state of the scan that
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment