Skip to content

Commit

Permalink
encoding/json: use append-like operations for encoding
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
dsnet authored and gopherbot committed Feb 24, 2023
1 parent e2f4134 commit 8e5f56a
Showing 1 changed file with 38 additions and 63 deletions.
101 changes: 38 additions & 63 deletions src/encoding/json/encode.go
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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 (
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

0 comments on commit 8e5f56a

Please sign in to comment.