]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: remove alloc when encoding short byte slices
authorDaniel Martí <mvdan@mvdan.cc>
Sat, 7 Jul 2018 20:40:28 +0000 (21:40 +0100)
committerDaniel Martí <mvdan@mvdan.cc>
Tue, 21 Aug 2018 09:05:51 +0000 (09:05 +0000)
If the encoded bytes fit in the bootstrap array encodeState.scratch, use
that instead of allocating a new byte slice.

Also tweaked the Encoding vs Encoder heuristic to use the length of the
encoded bytes, not the length of the input bytes. Encoding is used for
allocations of up to 1024 bytes, as we measured 2048 to be the point
where it no longer provides a noticeable advantage.

Also added some benchmarks. Only the first case changes in behavior.

name                 old time/op    new time/op    delta
MarshalBytes/32-4       420ns ± 1%     383ns ± 1%   -8.69%  (p=0.002 n=6+6)
MarshalBytes/256-4      913ns ± 1%     915ns ± 0%     ~     (p=0.580 n=5+6)
MarshalBytes/4096-4    7.72µs ± 0%    7.74µs ± 0%     ~     (p=0.340 n=5+6)

name                 old alloc/op   new alloc/op   delta
MarshalBytes/32-4        112B ± 0%       64B ± 0%  -42.86%  (p=0.002 n=6+6)
MarshalBytes/256-4       736B ± 0%      736B ± 0%     ~     (all equal)
MarshalBytes/4096-4    7.30kB ± 0%    7.30kB ± 0%     ~     (all equal)

name                 old allocs/op  new allocs/op  delta
MarshalBytes/32-4        2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.002 n=6+6)
MarshalBytes/256-4       2.00 ± 0%      2.00 ± 0%     ~     (all equal)
MarshalBytes/4096-4      2.00 ± 0%      2.00 ± 0%     ~     (all equal)

Updates #5683.

Change-Id: I5fa55c27bd7728338d770ae7c0756885ba9a5724
Reviewed-on: https://go-review.googlesource.com/122462
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
src/encoding/json/bench_test.go
src/encoding/json/encode.go

index bd322db2e6ffe05f2b7c16a05e95170c809fa56a..72cb349062c4a85c49bcc586c85c2f88813f04b7 100644 (file)
@@ -114,6 +114,34 @@ func BenchmarkCodeMarshal(b *testing.B) {
        b.SetBytes(int64(len(codeJSON)))
 }
 
+func benchMarshalBytes(n int) func(*testing.B) {
+       sample := []byte("hello world")
+       // Use a struct pointer, to avoid an allocation when passing it as an
+       // interface parameter to Marshal.
+       v := &struct {
+               Bytes []byte
+       }{
+               bytes.Repeat(sample, (n/len(sample))+1)[:n],
+       }
+       return func(b *testing.B) {
+               for i := 0; i < b.N; i++ {
+                       if _, err := Marshal(v); err != nil {
+                               b.Fatal("Marshal:", err)
+                       }
+               }
+       }
+}
+
+func BenchmarkMarshalBytes(b *testing.B) {
+       // 32 fits within encodeState.scratch.
+       b.Run("32", benchMarshalBytes(32))
+       // 256 doesn't fit in encodeState.scratch, but is small enough to
+       // allocate and avoid the slower base64.NewEncoder.
+       b.Run("256", benchMarshalBytes(256))
+       // 4096 is large enough that we want to avoid allocating for it.
+       b.Run("4096", benchMarshalBytes(4096))
+}
+
 func BenchmarkCodeDecoder(b *testing.B) {
        if codeJSON == nil {
                b.StopTimer()
index 632c12404a677c664eef16e60bd171ce8560a9db..d5fe4d6b78b232c6930b4b2c867b182fc85a029d 100644 (file)
@@ -718,14 +718,22 @@ func encodeByteSlice(e *encodeState, v reflect.Value, _ encOpts) {
        }
        s := v.Bytes()
        e.WriteByte('"')
-       if len(s) < 1024 {
-               // for small buffers, using Encode directly is much faster.
-               dst := make([]byte, base64.StdEncoding.EncodedLen(len(s)))
+       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 {
-               // for large buffers, avoid unnecessary extra temporary
-               // buffer space.
+               // 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()