Go version
go1.27rc1 darwin/arm64
Unexpected slowness
| Rank |
Library |
Total time |
Mean/pass |
Throughput |
ns/record |
Allocated bytes/record |
Allocs/record |
Relative to v1 |
| 1 |
encoding/json/v2 native |
983ms |
197ms |
213.32 MiB/s |
1967 |
1755 |
53.04 |
1.76x |
| 2 |
encoding/json (nojsonv2 v1) |
1.726s |
345ms |
121.55 MiB/s |
3451 |
2139 |
71.49 |
1.00x |
| 3 |
encoding/json (default jsonv2) |
2.216s |
443ms |
94.65 MiB/s |
4433 |
2089 |
69.84 |
0.78x |
We can see jsonv2 with v1 compatibility is roughly 20% slower.
How to reproduce
Benchmarked encoding/json.Unmarshal into any using Go 1.27rc1 default jsonv2-backed encoding/json and compared it with GOEXPERIMENT=nojsonv2.
The benchmark decodes NDJSON records into a fresh any:
var v any
if err := json.Unmarshal(record, &v); err != nil {
log.Fatal(err)
}
When there is no any in the data structure
| Rank |
Library |
Time |
Mean/pass |
Throughput |
ns/record |
Allocated bytes/record |
Allocs/record |
Relative to v1 |
| 1 |
encoding/json (default jsonv2) |
14.346s |
2.869s |
152.84 MiB/s |
28,691 |
7,149 |
131.42 |
1.69× |
| 2 |
encoding/json (nojsonv2 v1) |
24.203s |
4.841s |
90.59 MiB/s |
48,405 |
8,347 |
188.00 |
1.00× |
We can see the promised performance gain.
Profiling results
| Function |
Flat |
Flat % |
Cumulative |
Cumulative % |
Meaning |
encoding/json/v2.makeInterfaceArshaler.func2 |
390ms |
3.10% |
10.32s |
82.10% |
Generic interface decode path. |
encoding/json/v2.makeMapArshaler.func3 |
180ms |
1.43% |
10.18s |
80.99% |
Generic map decode path. |
encoding/json/v2.makeStringArshaler.func2 |
180ms |
1.43% |
2.29s |
18.22% |
Generic string decode path. |
runtime.mallocgc |
270ms |
2.15% |
2.19s |
17.42% |
Allocation and GC work. |
encoding/json/v2.makeSliceArshaler.func3 |
90ms |
0.72% |
2.13s |
16.95% |
Generic slice decode path. |
encoding/json/v2.newAddressableValue |
10ms |
0.08% |
1.64s |
13.05% |
Addressable reflection value construction. |
reflect.unsafe_New |
50ms |
0.40% |
1.55s |
12.33% |
Reflection allocation. |
reflect.New |
10ms |
0.08% |
1.50s |
11.93% |
Reflection allocation wrapper. |
reflect.Value.Set |
80ms |
0.64% |
1.35s |
10.74% |
Reflection assignment. |
reflect.Value.assignTo |
170ms |
1.35% |
1.29s |
10.26% |
Reflection conversion and assignment. |
reflect.Value.SetMapIndex |
130ms |
1.03% |
1.22s |
9.71% |
Reflection map insertion. |
This is caused by the extra reflection cost.
Fast path is skipped due to compatibility support
// encoding/json/v2/arshal_default.go
if optimizeCommon &&
t == anyType && !uo.Flags.Get(jsonflags.AllowDuplicateNames|jsonflags.FormatTag) &&
(uo.Unmarshalers == nil || !uo.Unmarshalers.(*Unmarshalers).fromAny) {
v, err := unmarshalValueAny(dec, uo)
if v != nil {
va.Set(reflect.ValueOf(v))
}
return err
}
Because AllowDuplicateNames is true, the optimized native v2 any path is skipped.
Go version
go1.27rc1 darwin/arm64
Unexpected slowness
encoding/json/v2 nativeencoding/json (nojsonv2 v1)encoding/json (default jsonv2)We can see
jsonv2with v1 compatibility is roughly 20% slower.How to reproduce
Benchmarked
encoding/json.Unmarshalintoanyusing Go 1.27rc1 default jsonv2-backedencoding/jsonand compared it withGOEXPERIMENT=nojsonv2.The benchmark decodes NDJSON records into a fresh
any:When there is no
anyin the data structureWe can see the promised performance gain.
Profiling results
encoding/json/v2.makeInterfaceArshaler.func2encoding/json/v2.makeMapArshaler.func3encoding/json/v2.makeStringArshaler.func2runtime.mallocgcencoding/json/v2.makeSliceArshaler.func3encoding/json/v2.newAddressableValuereflect.unsafe_Newreflect.Newreflect.Value.Setreflect.Value.assignToreflect.Value.SetMapIndexThis is caused by the extra
reflectioncost.Fast path is skipped due to compatibility support
Because
AllowDuplicateNamesis true, the optimized native v2anypath is skipped.