Skip to content

Commit 22d3770

Browse files
philpearlmvdan
authored andcommitted
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í <[email protected]> Run-TryBot: Daniel Martí <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 31bfab4 commit 22d3770

File tree

3 files changed

+60
-11
lines changed

3 files changed

+60
-11
lines changed

src/encoding/json/bench_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,22 @@ func BenchmarkTypeFieldsCache(b *testing.B) {
389389
})
390390
}
391391
}
392+
393+
func BenchmarkEncodeMarshaler(b *testing.B) {
394+
b.ReportAllocs()
395+
396+
m := struct {
397+
A int
398+
B RawMessage
399+
}{}
400+
401+
b.RunParallel(func(pb *testing.PB) {
402+
enc := NewEncoder(ioutil.Discard)
403+
404+
for pb.Next() {
405+
if err := enc.Encode(&m); err != nil {
406+
b.Fatal("Encode:", err)
407+
}
408+
}
409+
})
410+
}

src/encoding/json/indent.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
package json
66

7-
import "bytes"
7+
import (
8+
"bytes"
9+
)
810

911
// Compact appends to dst the JSON-encoded src with
1012
// insignificant space characters elided.
@@ -14,8 +16,8 @@ func Compact(dst *bytes.Buffer, src []byte) error {
1416

1517
func compact(dst *bytes.Buffer, src []byte, escape bool) error {
1618
origLen := dst.Len()
17-
var scan scanner
18-
scan.reset()
19+
scan := newScanner()
20+
defer freeScanner(scan)
1921
start := 0
2022
for i, c := range src {
2123
if escape && (c == '<' || c == '>' || c == '&') {
@@ -36,7 +38,7 @@ func compact(dst *bytes.Buffer, src []byte, escape bool) error {
3638
dst.WriteByte(hex[src[i+2]&0xF])
3739
start = i + 3
3840
}
39-
v := scan.step(&scan, c)
41+
v := scan.step(scan, c)
4042
if v >= scanSkipSpace {
4143
if v == scanError {
4244
break
@@ -78,13 +80,13 @@ func newline(dst *bytes.Buffer, prefix, indent string, depth int) {
7880
// if src ends in a trailing newline, so will dst.
7981
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
8082
origLen := dst.Len()
81-
var scan scanner
82-
scan.reset()
83+
scan := newScanner()
84+
defer freeScanner(scan)
8385
needIndent := false
8486
depth := 0
8587
for _, c := range src {
8688
scan.bytes++
87-
v := scan.step(&scan, c)
89+
v := scan.step(scan, c)
8890
if v == scanSkipSpace {
8991
continue
9092
}

src/encoding/json/scanner.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ package json
1313
// This file starts with two simple examples using the scanner
1414
// before diving into the scanner itself.
1515

16-
import "strconv"
16+
import (
17+
"strconv"
18+
"sync"
19+
)
1720

1821
// Valid reports whether data is a valid JSON encoding.
1922
func Valid(data []byte) bool {
20-
return checkValid(data, &scanner{}) == nil
23+
scan := newScanner()
24+
defer freeScanner(scan)
25+
return checkValid(data, scan) == nil
2126
}
2227

2328
// checkValid verifies that data is valid JSON-encoded data.
@@ -45,7 +50,7 @@ type SyntaxError struct {
4550
func (e *SyntaxError) Error() string { return e.msg }
4651

4752
// A scanner is a JSON scanning state machine.
48-
// Callers call scan.reset() and then pass bytes in one at a time
53+
// Callers call scan.reset and then pass bytes in one at a time
4954
// by calling scan.step(&scan, c) for each byte.
5055
// The return value, referred to as an opcode, tells the
5156
// caller about significant parsing events like beginning
@@ -72,10 +77,33 @@ type scanner struct {
7277
// Error that happened, if any.
7378
err error
7479

75-
// total bytes consumed, updated by decoder.Decode
80+
// total bytes consumed, updated by decoder.Decode (and deliberately
81+
// not set to zero by scan.reset)
7682
bytes int64
7783
}
7884

85+
var scannerPool = sync.Pool{
86+
New: func() interface{} {
87+
return &scanner{}
88+
},
89+
}
90+
91+
func newScanner() *scanner {
92+
scan := scannerPool.Get().(*scanner)
93+
// scan.reset by design doesn't set bytes to zero
94+
scan.bytes = 0
95+
scan.reset()
96+
return scan
97+
}
98+
99+
func freeScanner(scan *scanner) {
100+
// Avoid hanging on to too much memory in extreme cases.
101+
if len(scan.parseState) > 1024 {
102+
scan.parseState = nil
103+
}
104+
scannerPool.Put(scan)
105+
}
106+
79107
// These values are returned by the state transition functions
80108
// assigned to scanner.state and the method scanner.eof.
81109
// They give details about the current state of the scan that

0 commit comments

Comments
 (0)