Skip to content

encoding/json: Unmarshal into any is slower with jsonv2 compatibility than with nojsonv2 #80270

Description

@iandyh

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performance

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions