Skip to content

Commit 21ff670

Browse files
dsnetgopherbot
authored andcommitted
encoding/json: use append for Compact and Indent
This is part of the effort to reduce direct reliance on bytes.Buffer so that we can use a buffer with better pooling characteristics. Avoid direct use of bytes.Buffer in Compact and Indent and instead modify the logic to rely only on append. This avoids reliance on the bytes.Buffer.Truncate method, which makes switching to a custom buffer implementation easier. Performance: name old time/op new time/op delta EncodeMarshaler 25.5ns ± 8% 25.7ns ± 9% ~ (p=0.724 n=10+10) name old alloc/op new alloc/op delta EncodeMarshaler 4.00B ± 0% 4.00B ± 0% ~ (all equal) name old allocs/op new allocs/op delta EncodeMarshaler 1.00 ± 0% 1.00 ± 0% ~ (all equal) Updates #27735 Change-Id: I8cded03fab7651d43b5a238ee721f3472530868e Reviewed-on: https://go-review.googlesource.com/c/go/+/469555 Run-TryBot: Joseph Tsai <[email protected]> Reviewed-by: Daniel Martí <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Auto-Submit: Joseph Tsai <[email protected]> Reviewed-by: Bryan Mills <[email protected]>
1 parent e7c7f33 commit 21ff670

File tree

3 files changed

+69
-63
lines changed

3 files changed

+69
-63
lines changed

src/encoding/json/encode.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,12 @@ func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
175175
if err != nil {
176176
return nil, err
177177
}
178-
var buf bytes.Buffer
179-
err = Indent(&buf, b, prefix, indent)
178+
b2 := make([]byte, 0, indentGrowthFactor*len(b))
179+
b2, err = appendIndent(b2, b, prefix, indent)
180180
if err != nil {
181181
return nil, err
182182
}
183-
return buf.Bytes(), nil
183+
return b2, nil
184184
}
185185

186186
// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
@@ -476,8 +476,10 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
476476
}
477477
b, err := m.MarshalJSON()
478478
if err == nil {
479-
// copy JSON into buffer, checking validity.
480-
err = compact(&e.Buffer, b, opts.escapeHTML)
479+
e.Grow(len(b))
480+
out := availableBuffer(&e.Buffer)
481+
out, err = appendCompact(out, b, opts.escapeHTML)
482+
e.Buffer.Write(out)
481483
}
482484
if err != nil {
483485
e.error(&MarshalerError{v.Type(), err, "MarshalJSON"})
@@ -493,8 +495,10 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
493495
m := va.Interface().(Marshaler)
494496
b, err := m.MarshalJSON()
495497
if err == nil {
496-
// copy JSON into buffer, checking validity.
497-
err = compact(&e.Buffer, b, opts.escapeHTML)
498+
e.Grow(len(b))
499+
out := availableBuffer(&e.Buffer)
500+
out, err = appendCompact(out, b, opts.escapeHTML)
501+
e.Buffer.Write(out)
498502
}
499503
if err != nil {
500504
e.error(&MarshalerError{v.Type(), err, "MarshalJSON"})

src/encoding/json/indent.go

+55-49
Original file line numberDiff line numberDiff line change
@@ -4,69 +4,73 @@
44

55
package json
66

7-
import (
8-
"bytes"
9-
)
7+
import "bytes"
8+
9+
// TODO(https://go.dev/issue/53685): Use bytes.Buffer.AvailableBuffer instead.
10+
func availableBuffer(b *bytes.Buffer) []byte {
11+
return b.Bytes()[b.Len():]
12+
}
1013

1114
// Compact appends to dst the JSON-encoded src with
1215
// insignificant space characters elided.
1316
func Compact(dst *bytes.Buffer, src []byte) error {
14-
return compact(dst, src, false)
17+
dst.Grow(len(src))
18+
b := availableBuffer(dst)
19+
b, err := appendCompact(b, src, false)
20+
dst.Write(b)
21+
return err
1522
}
1623

17-
func compact(dst *bytes.Buffer, src []byte, escape bool) error {
18-
origLen := dst.Len()
24+
func appendCompact(dst, src []byte, escape bool) ([]byte, error) {
25+
origLen := len(dst)
1926
scan := newScanner()
2027
defer freeScanner(scan)
2128
start := 0
2229
for i, c := range src {
2330
if escape && (c == '<' || c == '>' || c == '&') {
24-
if start < i {
25-
dst.Write(src[start:i])
26-
}
27-
dst.WriteString(`\u00`)
28-
dst.WriteByte(hex[c>>4])
29-
dst.WriteByte(hex[c&0xF])
31+
dst = append(dst, src[start:i]...)
32+
dst = append(dst, '\\', 'u', '0', '0', hex[c>>4], hex[c&0xF])
3033
start = i + 1
3134
}
3235
// Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9).
3336
if escape && c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 {
34-
if start < i {
35-
dst.Write(src[start:i])
36-
}
37-
dst.WriteString(`\u202`)
38-
dst.WriteByte(hex[src[i+2]&0xF])
39-
start = i + 3
37+
dst = append(dst, src[start:i]...)
38+
dst = append(dst, '\\', 'u', '2', '0', '2', hex[src[i+2]&0xF])
39+
start = i + len("\u2029")
4040
}
4141
v := scan.step(scan, c)
4242
if v >= scanSkipSpace {
4343
if v == scanError {
4444
break
4545
}
46-
if start < i {
47-
dst.Write(src[start:i])
48-
}
46+
dst = append(dst, src[start:i]...)
4947
start = i + 1
5048
}
5149
}
5250
if scan.eof() == scanError {
53-
dst.Truncate(origLen)
54-
return scan.err
55-
}
56-
if start < len(src) {
57-
dst.Write(src[start:])
51+
return dst[:origLen], scan.err
5852
}
59-
return nil
53+
dst = append(dst, src[start:]...)
54+
return dst, nil
6055
}
6156

62-
func newline(dst *bytes.Buffer, prefix, indent string, depth int) {
63-
dst.WriteByte('\n')
64-
dst.WriteString(prefix)
57+
func appendNewline(dst []byte, prefix, indent string, depth int) []byte {
58+
dst = append(dst, '\n')
59+
dst = append(dst, prefix...)
6560
for i := 0; i < depth; i++ {
66-
dst.WriteString(indent)
61+
dst = append(dst, indent...)
6762
}
63+
return dst
6864
}
6965

66+
// indentGrowthFactor specifies the growth factor of indenting JSON input.
67+
// Empirically, the growth factor was measured to be between 1.4x to 1.8x
68+
// for some set of compacted JSON with the indent being a single tab.
69+
// Specify a growth factor slightly larger than what is observed
70+
// to reduce probability of allocation in appendIndent.
71+
// A factor no higher than 2 ensures that wasted space never exceeds 50%.
72+
const indentGrowthFactor = 2
73+
7074
// Indent appends to dst an indented form of the JSON-encoded src.
7175
// Each element in a JSON object or array begins on a new,
7276
// indented line beginning with prefix followed by one or more
@@ -79,7 +83,15 @@ func newline(dst *bytes.Buffer, prefix, indent string, depth int) {
7983
// For example, if src has no trailing spaces, neither will dst;
8084
// if src ends in a trailing newline, so will dst.
8185
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
82-
origLen := dst.Len()
86+
dst.Grow(indentGrowthFactor * len(src))
87+
b := availableBuffer(dst)
88+
b, err := appendIndent(b, src, prefix, indent)
89+
dst.Write(b)
90+
return err
91+
}
92+
93+
func appendIndent(dst, src []byte, prefix, indent string) ([]byte, error) {
94+
origLen := len(dst)
8395
scan := newScanner()
8496
defer freeScanner(scan)
8597
needIndent := false
@@ -96,13 +108,13 @@ func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
96108
if needIndent && v != scanEndObject && v != scanEndArray {
97109
needIndent = false
98110
depth++
99-
newline(dst, prefix, indent, depth)
111+
dst = appendNewline(dst, prefix, indent, depth)
100112
}
101113

102114
// Emit semantically uninteresting bytes
103115
// (in particular, punctuation in strings) unmodified.
104116
if v == scanContinue {
105-
dst.WriteByte(c)
117+
dst = append(dst, c)
106118
continue
107119
}
108120

@@ -111,33 +123,27 @@ func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
111123
case '{', '[':
112124
// delay indent so that empty object and array are formatted as {} and [].
113125
needIndent = true
114-
dst.WriteByte(c)
115-
126+
dst = append(dst, c)
116127
case ',':
117-
dst.WriteByte(c)
118-
newline(dst, prefix, indent, depth)
119-
128+
dst = append(dst, c)
129+
dst = appendNewline(dst, prefix, indent, depth)
120130
case ':':
121-
dst.WriteByte(c)
122-
dst.WriteByte(' ')
123-
131+
dst = append(dst, c, ' ')
124132
case '}', ']':
125133
if needIndent {
126134
// suppress indent in empty object/array
127135
needIndent = false
128136
} else {
129137
depth--
130-
newline(dst, prefix, indent, depth)
138+
dst = appendNewline(dst, prefix, indent, depth)
131139
}
132-
dst.WriteByte(c)
133-
140+
dst = append(dst, c)
134141
default:
135-
dst.WriteByte(c)
142+
dst = append(dst, c)
136143
}
137144
}
138145
if scan.eof() == scanError {
139-
dst.Truncate(origLen)
140-
return scan.err
146+
return dst[:origLen], scan.err
141147
}
142-
return nil
148+
return dst, nil
143149
}

src/encoding/json/stream.go

+3-7
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ type Encoder struct {
183183
err error
184184
escapeHTML bool
185185

186-
indentBuf *bytes.Buffer
186+
indentBuf []byte
187187
indentPrefix string
188188
indentValue string
189189
}
@@ -221,15 +221,11 @@ func (enc *Encoder) Encode(v any) error {
221221

222222
b := e.Bytes()
223223
if enc.indentPrefix != "" || enc.indentValue != "" {
224-
if enc.indentBuf == nil {
225-
enc.indentBuf = new(bytes.Buffer)
226-
}
227-
enc.indentBuf.Reset()
228-
err = Indent(enc.indentBuf, b, enc.indentPrefix, enc.indentValue)
224+
enc.indentBuf, err = appendIndent(enc.indentBuf[:0], b, enc.indentPrefix, enc.indentValue)
229225
if err != nil {
230226
return err
231227
}
232-
b = enc.indentBuf.Bytes()
228+
b = enc.indentBuf
233229
}
234230
if _, err = enc.w.Write(b); err != nil {
235231
enc.err = err

0 commit comments

Comments
 (0)