aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Tsai <joetsai@digital-static.net>2026-01-30 12:15:31 -0800
committerJoseph Tsai <joetsai@digital-static.net>2026-02-02 13:05:45 -0800
commit35abaf75c35adc9b22038885781b8be70a8476e0 (patch)
tree6f0340cc0277f98012ed45410d6852cc6bcf7754
parent07f7f8ca5202bdcd39216e25b4a5b2c309ea727f (diff)
downloadgo-35abaf75c35adc9b22038885781b8be70a8476e0.tar.xz
encoding/json: use pooled encoder in Encoder.Encode
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>
-rw-r--r--src/encoding/json/bench_test.go17
-rw-r--r--src/encoding/json/v2_bench_test.go18
-rw-r--r--src/encoding/json/v2_stream.go19
3 files changed, 46 insertions, 8 deletions
diff --git a/src/encoding/json/bench_test.go b/src/encoding/json/bench_test.go
index 047188131c..4e3bfe8bd2 100644
--- a/src/encoding/json/bench_test.go
+++ b/src/encoding/json/bench_test.go
@@ -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)
+ }
+ }
+}
diff --git a/src/encoding/json/v2_bench_test.go b/src/encoding/json/v2_bench_test.go
index b9ed7b6220..7057f9bec4 100644
--- a/src/encoding/json/v2_bench_test.go
+++ b/src/encoding/json/v2_bench_test.go
@@ -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)
+ }
+ }
+}
diff --git a/src/encoding/json/v2_stream.go b/src/encoding/json/v2_stream.go
index ca0822cb73..ee16629cbb 100644
--- a/src/encoding/json/v2_stream.go
+++ b/src/encoding/json/v2_stream.go
@@ -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
}