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/+/200864
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/src/encoding/json/bench_test.go b/src/encoding/json/bench_test.go
index f92d39f..4a5fe7e 100644
--- a/src/encoding/json/bench_test.go
+++ b/src/encoding/json/bench_test.go
@@ -389,3 +389,22 @@
})
}
}
+
+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)
+ }
+ }
+ })
+}
diff --git a/src/encoding/json/indent.go b/src/encoding/json/indent.go
index 06adfc1..2924d3b 100644
--- a/src/encoding/json/indent.go
+++ b/src/encoding/json/indent.go
@@ -4,7 +4,9 @@
package json
-import "bytes"
+import (
+ "bytes"
+)
// Compact appends to dst the JSON-encoded src with
// insignificant space characters elided.
@@ -14,8 +16,8 @@
func compact(dst *bytes.Buffer, src []byte, escape bool) error {
origLen := dst.Len()
- var scan scanner
- scan.reset()
+ scan := newScanner()
+ defer freeScanner(scan)
start := 0
for i, c := range src {
if escape && (c == '<' || c == '>' || c == '&') {
@@ -36,7 +38,7 @@
dst.WriteByte(hex[src[i+2]&0xF])
start = i + 3
}
- v := scan.step(&scan, c)
+ v := scan.step(scan, c)
if v >= scanSkipSpace {
if v == scanError {
break
@@ -78,13 +80,13 @@
// if src ends in a trailing newline, so will dst.
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
origLen := dst.Len()
- var scan scanner
- scan.reset()
+ scan := newScanner()
+ defer freeScanner(scan)
needIndent := false
depth := 0
for _, c := range src {
scan.bytes++
- v := scan.step(&scan, c)
+ v := scan.step(scan, c)
if v == scanSkipSpace {
continue
}
diff --git a/src/encoding/json/scanner.go b/src/encoding/json/scanner.go
index 8857224..552bd70 100644
--- a/src/encoding/json/scanner.go
+++ b/src/encoding/json/scanner.go
@@ -13,11 +13,16 @@
// This file starts with two simple examples using the scanner
// before diving into the scanner itself.
-import "strconv"
+import (
+ "strconv"
+ "sync"
+)
// Valid reports whether data is a valid JSON encoding.
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.
@@ -45,7 +50,7 @@
func (e *SyntaxError) Error() string { return e.msg }
// 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.
// The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning
@@ -72,10 +77,33 @@
// Error that happened, if any.
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
}
+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
// assigned to scanner.state and the method scanner.eof.
// They give details about the current state of the scan that