]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: use pooled encoder in Encoder.Encode
authorJoe Tsai <joetsai@digital-static.net>
Fri, 30 Jan 2026 20:15:31 +0000 (12:15 -0800)
committerJoseph Tsai <joetsai@digital-static.net>
Mon, 2 Feb 2026 21:05:45 +0000 (13:05 -0800)
Due to the lack of MarshalWrite in the v1 API,
it is unfortunately common to see:

    json.NewEncoder(out).Encode(in)

where a single-use Encoder is constructed and thrown away.
This performed acceptably in v1 since every call to Encode
used a globally pooled encoder resource.

Prior to this change, the v1-in-v2 implementation relied
on a bytes.Buffer cached only for the lifetime of the Encoder
object itself. Thus, a single-use Encoder does not benefit.
Modify the wrapper implementation to use the internal
pooled encoder from v2 and use the intermediate buffer
to write directly to the output io.Writer.

We assume that the user-provided io.Writer never leaks the buffer,
but this assumption was already held in the v1 implementation.
We are not increasing the surface area of data corruption risk.

Performance of v1 to v1-in-v2 (before the pool fix):

name                 old time/op    new time/op    delta
NewEncoderEncode-32    30.2ms ± 4%    28.3ms ± 9%    -6.19%  (p=0.002 n=9+10)

name                 old alloc/op   new alloc/op   delta
NewEncoderEncode-32    7.64MB ± 0%   28.37MB ± 0%  +271.23%  (p=0.000 n=10+10)

name                 old allocs/op  new allocs/op  delta
NewEncoderEncode-32      200k ± 0%      100k ± 0%   -49.99%  (p=0.000 n=9+10)

Interestingly, v1-in-2 is slightly faster,
but the amount of allocated memory is massive.

Performance of v1 to v1-in-v2 (after the pool fix):

name                 old time/op    new time/op    delta
NewEncoderEncode-32    30.2ms ± 4%    24.0ms ± 7%  -20.36%  (p=0.000 n=9+10)

name                 old alloc/op   new alloc/op   delta
NewEncoderEncode-32    7.64MB ± 0%    4.09MB ± 3%  -46.52%  (p=0.000 n=10+10)

name                 old allocs/op  new allocs/op  delta
NewEncoderEncode-32      200k ± 0%      100k ± 0%  -50.00%  (p=0.000 n=9+9)

Now, the v1-in-v2 implementation is better than v1 on all metrics.

Fixes #75026

Change-Id: I50c975b1d5b8da806e46bc627966b0a39c1817eb
Reviewed-on: https://go-review.googlesource.com/c/go/+/740660
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

src/encoding/json/bench_test.go
src/encoding/json/v2_bench_test.go
src/encoding/json/v2_stream.go

index 047188131cecf2619a6113803b2bd825ac1c9e22..4e3bfe8bd2e60261536d53fc0358cb2eca6b4081 100644 (file)
@@ -14,6 +14,7 @@ package json
 
 import (
        "bytes"
+       "crypto/sha256"
        "fmt"
        "internal/testenv"
        "internal/zstd"
@@ -581,3 +582,19 @@ func BenchmarkUnmarshalNumber(b *testing.B) {
                }
        }
 }
+
+func BenchmarkNewEncoderEncode(b *testing.B) {
+       m := make(map[string]string)
+       for i := range 100_000 {
+               k := fmt.Sprintf("key%d", i)
+               v := fmt.Sprintf("%x", sha256.Sum256([]byte(k)))
+               m[k] = v
+       }
+       b.ResetTimer()
+       b.ReportAllocs()
+       for b.Loop() {
+               if err := NewEncoder(io.Discard).Encode(m); err != nil {
+                       b.Fatalf("Encode error: %v", err)
+               }
+       }
+}
index b9ed7b62207be13a352f887195e5ac16f15fb8b6..7057f9bec45d55fffedd1379d3de4e8b145bd403 100644 (file)
@@ -14,6 +14,8 @@ package json
 
 import (
        "bytes"
+       "crypto/sha256"
+       "fmt"
        "io"
        "strings"
        "testing"
@@ -481,3 +483,19 @@ func BenchmarkEncoderEncode(b *testing.B) {
                }
        })
 }
+
+func BenchmarkNewEncoderEncode(b *testing.B) {
+       m := make(map[string]string)
+       for i := range 100_000 {
+               k := fmt.Sprintf("key%d", i)
+               v := fmt.Sprintf("%x", sha256.Sum256([]byte(k)))
+               m[k] = v
+       }
+       b.ResetTimer()
+       b.ReportAllocs()
+       for b.Loop() {
+               if err := NewEncoder(io.Discard).Encode(m); err != nil {
+                       b.Fatalf("Encode error: %v", err)
+               }
+       }
+}
index ca0822cb7336cba4d5ddd87b6c58d1475ac4cae4..ee16629cbb3ec8677b960463888deacbfb5e7da3 100644 (file)
@@ -96,7 +96,6 @@ type Encoder struct {
        opts jsonv2.Options
        err  error
 
-       buf       bytes.Buffer
        indentBuf bytes.Buffer
 
        indentPrefix string
@@ -121,21 +120,22 @@ func (enc *Encoder) Encode(v any) error {
                return enc.err
        }
 
-       buf := &enc.buf
-       buf.Reset()
-       if err := jsonv2.MarshalWrite(buf, v, enc.opts); err != nil {
+       e := export.GetBufferedEncoder(enc.opts)
+       defer export.PutBufferedEncoder(e)
+       if err := jsonv2.MarshalEncode(e, v); err != nil {
                return err
        }
+       b := export.Encoder(e).Buf // b must not leak current scope
        if len(enc.indentPrefix)+len(enc.indentValue) > 0 {
                enc.indentBuf.Reset()
-               if err := Indent(&enc.indentBuf, buf.Bytes(), enc.indentPrefix, enc.indentValue); err != nil {
+               if err := Indent(&enc.indentBuf, b, enc.indentPrefix, enc.indentValue); err != nil {
                        return err
                }
-               buf = &enc.indentBuf
+               b = enc.indentBuf.Bytes()
        }
-       buf.WriteByte('\n')
+       b = append(b, '\n')
 
-       if _, err := enc.w.Write(buf.Bytes()); err != nil {
+       if _, err := enc.w.Write(b); err != nil {
                enc.err = err
                return err
        }
@@ -146,6 +146,9 @@ func (enc *Encoder) Encode(v any) error {
 // value as if indented by the package-level function Indent(dst, src, prefix, indent).
 // Calling SetIndent("", "") disables indentation.
 func (enc *Encoder) SetIndent(prefix, indent string) {
+       // NOTE: Do not rely on the newer [jsontext.WithIndent] option since
+       // the v1 [Indent] behavior has historical bugs that cannot be changed
+       // for backward compatibility reasons.
        enc.indentPrefix = prefix
        enc.indentValue = indent
 }