-
Notifications
You must be signed in to change notification settings - Fork 18.3k
Description
Go version
go version go1.26-devel_a8564bd Thu Aug 14 12:20:59 2025 -0700 darwin/arm64
Output of go env
in your module/workspace:
AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE='auto'
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/okokes/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/okokes/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/s3/wfx4nrj12bz2pmxlr1sfymgm0000gp/T/go-build766535619=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD=''
GOMODCACHE='/Users/okokes/go/pkg/mod'
GONOPROXY='github.com/rapid7'
GONOSUMDB='github.com/rapid7'
GOOS='darwin'
GOPATH='/Users/okokes/go'
GOPRIVATE='github.com/rapid7'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/okokes/sdk/gotip'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/okokes/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/okokes/sdk/gotip/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.26-devel_a8564bd Thu Aug 14 12:20:59 2025 -0700'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
I tried enabling the jsonv2
experiment both with and without the use of the new API. I noticed that when not using the new API (i.e. no code changes, just flipping a GOEXPERIMENT
env), there was a huge increase memory allocations (in terms of bytes). Allocation count actually dropped significantly and the encoding peformance is a ~wash (or slightly slower with jsonv2).
It is my understanding that going forward (i.e. once the GOEXPERIMENT is retired), encoding/json
will default to the v2 implementation under the hood (keeping the same v1 API). So once that happens, people upgrading to 1.26+ while still using the v1 API will see a sizable increase in allocation unless this is addressed.
I tried go1.25rc2, go1.25.0 and gotip (a8564bd), reproduced it in all three.
What did you see happen?
I'm encoding map[string]string
values and the huge increase in allocations (in terms of bytes) is evident as we're growing the value length (though the allocations do grow even for short values).
go version go1.26-devel_a8564bd Thu Aug 14 12:20:59 2025 -0700 darwin/arm64
goos: darwin
goarch: arm64
cpu: Apple M3 Pro
│ bench-baseline.txt │ bench-jsonv2.txt │
│ sec/op │ sec/op vs base │
Encoding/keyCount=10,valLength=10 1.008µ ± 10% 1.230µ ± 1% +22.02% (p=0.000 n=10)
Encoding/keyCount=10,valLength=1000 6.414µ ± 2% 8.118µ ± 1% +26.56% (p=0.000 n=10)
Encoding/keyCount=1000,valLength=10 184.9µ ± 3% 179.6µ ± 1% -2.88% (p=0.000 n=10)
Encoding/keyCount=1000,valLength=1000 741.7µ ± 1% 841.8µ ± 1% +13.50% (p=0.000 n=10)
Encoding/keyCount=100000,valLength=10 30.18m ± 2% 25.14m ± 3% -16.72% (p=0.000 n=10)
Encoding/keyCount=100000,valLength=1000 84.41m ± 2% 79.22m ± 2% -6.15% (p=0.001 n=10)
geomean 362.2µ 379.9µ +4.87%
│ bench-baseline.txt │ bench-jsonv2.txt │
│ B/op │ B/op vs base │
Encoding/keyCount=10,valLength=10 832.0 ± 0% 840.0 ± 0% +0.96% (p=0.000 n=10)
Encoding/keyCount=10,valLength=1000 832.0 ± 0% 33139.0 ± 0% +3883.05% (p=0.000 n=10)
Encoding/keyCount=1000,valLength=10 71.35Ki ± 0% 79.80Ki ± 0% +11.84% (p=0.000 n=10)
Encoding/keyCount=1000,valLength=1000 71.35Ki ± 2% 4112.05Ki ± 0% +5663.40% (p=0.000 n=10)
Encoding/keyCount=100000,valLength=10 7.077Mi ± 0% 11.058Mi ± 0% +56.24% (p=0.000 n=10)
Encoding/keyCount=100000,valLength=1000 26.57Mi ± 6% 259.06Mi ± 0% +875.15% (p=0.000 n=10)
geomean 93.37Ki 544.9Ki +483.56%
│ bench-baseline.txt │ bench-jsonv2.txt │
│ allocs/op │ allocs/op vs base │
Encoding/keyCount=10,valLength=10 22.00 ± 0% 19.00 ± 0% -13.64% (p=0.000 n=10)
Encoding/keyCount=10,valLength=1000 22.00 ± 0% 21.00 ± 0% -4.55% (p=0.000 n=10)
Encoding/keyCount=1000,valLength=10 2.002k ± 0% 1.016k ± 0% -49.25% (p=0.000 n=10)
Encoding/keyCount=1000,valLength=1000 2.002k ± 0% 1.021k ± 0% -49.00% (p=0.000 n=10)
Encoding/keyCount=100000,valLength=10 200.0k ± 0% 100.0k ± 0% -49.99% (p=0.000 n=10)
Encoding/keyCount=100000,valLength=1000 200.0k ± 0% 100.0k ± 0% -49.99% (p=0.000 n=10)
geomean 2.065k 1.267k -38.64%
Code
package main
import (
"encoding/json"
"fmt"
"io"
"math/rand"
"testing"
)
func BenchmarkEncoding(b *testing.B) {
for _, keyCount := range []int{10, 1000, 100_000} {
for _, valLength := range []int{10, 1000} {
b.Run(fmt.Sprintf("keyCount=%d,valLength=%d", keyCount, valLength), func(b *testing.B) {
dt := make(map[string]string, keyCount)
for k := range keyCount {
val := make([]byte, valLength)
for j := 0; j < valLength; j++ {
val[j] = 'a' + byte(rand.Intn(26))
}
dt[fmt.Sprintf("key%d", k)] = string(val)
}
b.ResetTimer()
for b.Loop() {
if err := json.NewEncoder(io.Discard).Encode(dt); err != nil {
// if err := json.MarshalWrite(io.Discard, dt); err != nil {
b.Fatal(err)
}
}
})
}
}
}
Wrapper shell script
#!/usr/bin/env bash
set -eu
COUNT=10
GOMAXPROCS=1 gotip test -count=$COUNT -bench=. -benchmem -run=NONE . | tee bench-baseline.txt
GOEXPERIMENT=jsonv2 GOMAXPROCS=1 gotip test -count=$COUNT -bench=. -benchmem -run=NONE . | tee bench-jsonv2.txt
gotip version
benchstat bench-baseline.txt bench-jsonv2.txt
Looking at the memory profile didn't show anything immediately suspicious, but I haven't dug around it too much.
(pprof) top
Showing nodes accounting for 15.48GB, 99.68% of 15.53GB total
Dropped 8 nodes (cum <= 0.08GB)
Showing top 10 nodes out of 19
flat flat% sum% cum cum%
14.44GB 92.98% 92.98% 14.44GB 92.98% bytes.growSlice
0.37GB 2.41% 95.38% 0.37GB 2.41% reflect.copyVal
0.20GB 1.27% 96.66% 15.53GB 100% /Users/okokes/git/personal/perf.BenchmarkEncoding.func1
0.16GB 1.06% 97.72% 0.16GB 1.06% encoding/json.NewEncoder
0.15GB 0.98% 98.69% 0.15GB 0.98% slices.Grow[go.shape.[]uint8,go.shape.uint8] (inline)
0.09GB 0.59% 99.28% 0.09GB 0.59% encoding/json/v2.getStrings
0.06GB 0.4% 99.68% 14.50GB 93.38% bytes.(*Buffer).grow
0 0% 99.68% 14.31GB 92.15% bytes.(*Buffer).Grow (inline)
0 0% 99.68% 0.19GB 1.23% bytes.(*Buffer).Write
0 0% 99.68% 15.17GB 97.67% encoding/json.(*Encoder).Encode
What did you expect to see?
I expected the memory allocations to be the same or better than when the experiment is turned off.