Skip to content

Commit 8e5f56a

Browse files
dsnetgopherbot
authored andcommitted
encoding/json: use append-like operations for encoding
As part of the effort to rely less on bytes.Buffer, switch most operations to use more natural append-like operations. This makes it easier to swap bytes.Buffer out with a buffer type that only needs to support a minimal subset of operations. As a simplification, we can remove the use of the scratch buffer and use the available capacity of the buffer itself as the scratch. Also, declare an inlineable mayAppendQuote function to conditionally append a double-quote if necessary. Performance: name old time/op new time/op delta CodeEncoder 405µs ± 2% 397µs ± 2% -1.94% (p=0.000 n=20+20) CodeEncoderError 453µs ± 1% 444µs ± 4% -1.83% (p=0.000 n=19+19) CodeMarshal 559µs ± 4% 548µs ± 2% -2.02% (p=0.001 n=19+17) CodeMarshalError 724µs ± 3% 716µs ± 2% -1.13% (p=0.030 n=19+20) EncodeMarshaler 24.9ns ±15% 22.9ns ± 5% ~ (p=0.086 n=20+17) EncoderEncode 14.0ns ±27% 15.0ns ±20% ~ (p=0.365 n=20+20) There is a slight performance gain across the board due to the elimination of the scratch buffer. Appends are done directly into the unused capacity of the underlying buffer, avoiding an additional copy. See #53685 Updates #27735 Change-Id: Icf6d612a7f7a51ecd10097af092762dd1225d49e Reviewed-on: https://go-review.googlesource.com/c/go/+/469558 Reviewed-by: Daniel Martí <[email protected]> Auto-Submit: Joseph Tsai <[email protected]> Reviewed-by: Bryan Mills <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Run-TryBot: Joseph Tsai <[email protected]>
1 parent e2f4134 commit 8e5f56a

File tree

1 file changed

+38
-63
lines changed

1 file changed

+38
-63
lines changed

src/encoding/json/encode.go

+38-63
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,6 @@ var hex = "0123456789abcdef"
284284
// An encodeState encodes JSON into a bytes.Buffer.
285285
type encodeState struct {
286286
bytes.Buffer // accumulated output
287-
scratch [64]byte
288287

289288
// Keep track of what pointers we've seen in the current recursive call
290289
// path, to avoid cycles that could lead to a stack overflow. Only do
@@ -345,7 +344,7 @@ func isEmptyValue(v reflect.Value) bool {
345344
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
346345
return v.Len() == 0
347346
case reflect.Bool:
348-
return !v.Bool()
347+
return v.Bool() == false
349348
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
350349
return v.Int() == 0
351350
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
@@ -541,39 +540,27 @@ func addrTextMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
541540
}
542541

543542
func boolEncoder(e *encodeState, v reflect.Value, opts encOpts) {
544-
if opts.quoted {
545-
e.WriteByte('"')
546-
}
547-
if v.Bool() {
548-
e.WriteString("true")
549-
} else {
550-
e.WriteString("false")
551-
}
552-
if opts.quoted {
553-
e.WriteByte('"')
554-
}
543+
b := e.AvailableBuffer()
544+
b = mayAppendQuote(b, opts.quoted)
545+
b = strconv.AppendBool(b, v.Bool())
546+
b = mayAppendQuote(b, opts.quoted)
547+
e.Write(b)
555548
}
556549

557550
func intEncoder(e *encodeState, v reflect.Value, opts encOpts) {
558-
b := strconv.AppendInt(e.scratch[:0], v.Int(), 10)
559-
if opts.quoted {
560-
e.WriteByte('"')
561-
}
551+
b := e.AvailableBuffer()
552+
b = mayAppendQuote(b, opts.quoted)
553+
b = strconv.AppendInt(b, v.Int(), 10)
554+
b = mayAppendQuote(b, opts.quoted)
562555
e.Write(b)
563-
if opts.quoted {
564-
e.WriteByte('"')
565-
}
566556
}
567557

568558
func uintEncoder(e *encodeState, v reflect.Value, opts encOpts) {
569-
b := strconv.AppendUint(e.scratch[:0], v.Uint(), 10)
570-
if opts.quoted {
571-
e.WriteByte('"')
572-
}
559+
b := e.AvailableBuffer()
560+
b = mayAppendQuote(b, opts.quoted)
561+
b = strconv.AppendUint(b, v.Uint(), 10)
562+
b = mayAppendQuote(b, opts.quoted)
573563
e.Write(b)
574-
if opts.quoted {
575-
e.WriteByte('"')
576-
}
577564
}
578565

579566
type floatEncoder int // number of bits
@@ -589,7 +576,8 @@ func (bits floatEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
589576
// See golang.org/issue/6384 and golang.org/issue/14135.
590577
// Like fmt %g, but the exponent cutoffs are different
591578
// and exponents themselves are not padded to two digits.
592-
b := e.scratch[:0]
579+
b := e.AvailableBuffer()
580+
b = mayAppendQuote(b, opts.quoted)
593581
abs := math.Abs(f)
594582
fmt := byte('f')
595583
// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
@@ -607,14 +595,8 @@ func (bits floatEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
607595
b = b[:n-1]
608596
}
609597
}
610-
611-
if opts.quoted {
612-
e.WriteByte('"')
613-
}
598+
b = mayAppendQuote(b, opts.quoted)
614599
e.Write(b)
615-
if opts.quoted {
616-
e.WriteByte('"')
617-
}
618600
}
619601

620602
var (
@@ -633,13 +615,11 @@ func stringEncoder(e *encodeState, v reflect.Value, opts encOpts) {
633615
if !isValidNumber(numStr) {
634616
e.error(fmt.Errorf("json: invalid number literal %q", numStr))
635617
}
636-
if opts.quoted {
637-
e.WriteByte('"')
638-
}
639-
e.WriteString(numStr)
640-
if opts.quoted {
641-
e.WriteByte('"')
642-
}
618+
b := e.AvailableBuffer()
619+
b = mayAppendQuote(b, opts.quoted)
620+
b = append(b, numStr...)
621+
b = mayAppendQuote(b, opts.quoted)
622+
e.Write(b)
643623
return
644624
}
645625
if opts.quoted {
@@ -839,28 +819,16 @@ func encodeByteSlice(e *encodeState, v reflect.Value, _ encOpts) {
839819
return
840820
}
841821
s := v.Bytes()
842-
e.WriteByte('"')
843822
encodedLen := base64.StdEncoding.EncodedLen(len(s))
844-
if encodedLen <= len(e.scratch) {
845-
// If the encoded bytes fit in e.scratch, avoid an extra
846-
// allocation and use the cheaper Encoding.Encode.
847-
dst := e.scratch[:encodedLen]
848-
base64.StdEncoding.Encode(dst, s)
849-
e.Write(dst)
850-
} else if encodedLen <= 1024 {
851-
// The encoded bytes are short enough to allocate for, and
852-
// Encoding.Encode is still cheaper.
853-
dst := make([]byte, encodedLen)
854-
base64.StdEncoding.Encode(dst, s)
855-
e.Write(dst)
856-
} else {
857-
// The encoded bytes are too long to cheaply allocate, and
858-
// Encoding.Encode is no longer noticeably cheaper.
859-
enc := base64.NewEncoder(base64.StdEncoding, e)
860-
enc.Write(s)
861-
enc.Close()
862-
}
863-
e.WriteByte('"')
823+
e.Grow(len(`"`) + encodedLen + len(`"`))
824+
825+
// TODO(https://go.dev/issue/53693): Use base64.Encoding.AppendEncode.
826+
b := e.AvailableBuffer()
827+
b = append(b, '"')
828+
base64.StdEncoding.Encode(b[len(b):][:encodedLen], s)
829+
b = b[:len(b)+encodedLen]
830+
b = append(b, '"')
831+
e.Write(b)
864832
}
865833

866834
// sliceEncoder just wraps an arrayEncoder, checking to make sure the value isn't nil.
@@ -1343,3 +1311,10 @@ func cachedTypeFields(t reflect.Type) structFields {
13431311
f, _ := fieldCache.LoadOrStore(t, typeFields(t))
13441312
return f.(structFields)
13451313
}
1314+
1315+
func mayAppendQuote(b []byte, quoted bool) []byte {
1316+
if quoted {
1317+
b = append(b, '"')
1318+
}
1319+
return b
1320+
}

0 commit comments

Comments
 (0)