]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: use append-like operations for encoding
authorJoe Tsai <joetsai@digital-static.net>
Mon, 20 Feb 2023 03:05:12 +0000 (19:05 -0800)
committerGopher Robot <gobot@golang.org>
Fri, 24 Feb 2023 19:16:21 +0000 (19:16 +0000)
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í <mvdan@mvdan.cc>
Auto-Submit: Joseph Tsai <joetsai@digital-static.net>
Reviewed-by: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Run-TryBot: Joseph Tsai <joetsai@digital-static.net>

src/encoding/json/encode.go

index f7cfb2b820584b590a9f396186a0506049df185e..ff0e40d532e87323318afbf9ffaf322566b65246 100644 (file)
@@ -284,7 +284,6 @@ var hex = "0123456789abcdef"
 // An encodeState encodes JSON into a bytes.Buffer.
 type encodeState struct {
        bytes.Buffer // accumulated output
-       scratch      [64]byte
 
        // Keep track of what pointers we've seen in the current recursive call
        // path, to avoid cycles that could lead to a stack overflow. Only do
@@ -345,7 +344,7 @@ func isEmptyValue(v reflect.Value) bool {
        case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
                return v.Len() == 0
        case reflect.Bool:
-               return !v.Bool()
+               return v.Bool() == false
        case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                return v.Int() == 0
        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) {
 }
 
 func boolEncoder(e *encodeState, v reflect.Value, opts encOpts) {
-       if opts.quoted {
-               e.WriteByte('"')
-       }
-       if v.Bool() {
-               e.WriteString("true")
-       } else {
-               e.WriteString("false")
-       }
-       if opts.quoted {
-               e.WriteByte('"')
-       }
+       b := e.AvailableBuffer()
+       b = mayAppendQuote(b, opts.quoted)
+       b = strconv.AppendBool(b, v.Bool())
+       b = mayAppendQuote(b, opts.quoted)
+       e.Write(b)
 }
 
 func intEncoder(e *encodeState, v reflect.Value, opts encOpts) {
-       b := strconv.AppendInt(e.scratch[:0], v.Int(), 10)
-       if opts.quoted {
-               e.WriteByte('"')
-       }
+       b := e.AvailableBuffer()
+       b = mayAppendQuote(b, opts.quoted)
+       b = strconv.AppendInt(b, v.Int(), 10)
+       b = mayAppendQuote(b, opts.quoted)
        e.Write(b)
-       if opts.quoted {
-               e.WriteByte('"')
-       }
 }
 
 func uintEncoder(e *encodeState, v reflect.Value, opts encOpts) {
-       b := strconv.AppendUint(e.scratch[:0], v.Uint(), 10)
-       if opts.quoted {
-               e.WriteByte('"')
-       }
+       b := e.AvailableBuffer()
+       b = mayAppendQuote(b, opts.quoted)
+       b = strconv.AppendUint(b, v.Uint(), 10)
+       b = mayAppendQuote(b, opts.quoted)
        e.Write(b)
-       if opts.quoted {
-               e.WriteByte('"')
-       }
 }
 
 type floatEncoder int // number of bits
@@ -589,7 +576,8 @@ func (bits floatEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
        // See golang.org/issue/6384 and golang.org/issue/14135.
        // Like fmt %g, but the exponent cutoffs are different
        // and exponents themselves are not padded to two digits.
-       b := e.scratch[:0]
+       b := e.AvailableBuffer()
+       b = mayAppendQuote(b, opts.quoted)
        abs := math.Abs(f)
        fmt := byte('f')
        // 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) {
                        b = b[:n-1]
                }
        }
-
-       if opts.quoted {
-               e.WriteByte('"')
-       }
+       b = mayAppendQuote(b, opts.quoted)
        e.Write(b)
-       if opts.quoted {
-               e.WriteByte('"')
-       }
 }
 
 var (
@@ -633,13 +615,11 @@ func stringEncoder(e *encodeState, v reflect.Value, opts encOpts) {
                if !isValidNumber(numStr) {
                        e.error(fmt.Errorf("json: invalid number literal %q", numStr))
                }
-               if opts.quoted {
-                       e.WriteByte('"')
-               }
-               e.WriteString(numStr)
-               if opts.quoted {
-                       e.WriteByte('"')
-               }
+               b := e.AvailableBuffer()
+               b = mayAppendQuote(b, opts.quoted)
+               b = append(b, numStr...)
+               b = mayAppendQuote(b, opts.quoted)
+               e.Write(b)
                return
        }
        if opts.quoted {
@@ -839,28 +819,16 @@ func encodeByteSlice(e *encodeState, v reflect.Value, _ encOpts) {
                return
        }
        s := v.Bytes()
-       e.WriteByte('"')
        encodedLen := base64.StdEncoding.EncodedLen(len(s))
-       if encodedLen <= len(e.scratch) {
-               // If the encoded bytes fit in e.scratch, avoid an extra
-               // allocation and use the cheaper Encoding.Encode.
-               dst := e.scratch[:encodedLen]
-               base64.StdEncoding.Encode(dst, s)
-               e.Write(dst)
-       } else if encodedLen <= 1024 {
-               // The encoded bytes are short enough to allocate for, and
-               // Encoding.Encode is still cheaper.
-               dst := make([]byte, encodedLen)
-               base64.StdEncoding.Encode(dst, s)
-               e.Write(dst)
-       } else {
-               // The encoded bytes are too long to cheaply allocate, and
-               // Encoding.Encode is no longer noticeably cheaper.
-               enc := base64.NewEncoder(base64.StdEncoding, e)
-               enc.Write(s)
-               enc.Close()
-       }
-       e.WriteByte('"')
+       e.Grow(len(`"`) + encodedLen + len(`"`))
+
+       // TODO(https://go.dev/issue/53693): Use base64.Encoding.AppendEncode.
+       b := e.AvailableBuffer()
+       b = append(b, '"')
+       base64.StdEncoding.Encode(b[len(b):][:encodedLen], s)
+       b = b[:len(b)+encodedLen]
+       b = append(b, '"')
+       e.Write(b)
 }
 
 // sliceEncoder just wraps an arrayEncoder, checking to make sure the value isn't nil.
@@ -1343,3 +1311,10 @@ func cachedTypeFields(t reflect.Type) structFields {
        f, _ := fieldCache.LoadOrStore(t, typeFields(t))
        return f.(structFields)
 }
+
+func mayAppendQuote(b []byte, quoted bool) []byte {
+       if quoted {
+               b = append(b, '"')
+       }
+       return b
+}