Azure / azure-sdk-for-go

This repository is for active development of the Azure SDK for Go. For consumers of the SDK we recommend visiting our public developer docs at:
https://docs.microsoft.com/azure/developer/go/
MIT License
1.65k stars 845 forks source link

Perf: remove reliance on in-box JSON marshaller #19356

Open jhendrixMSFT opened 2 years ago

jhendrixMSFT commented 2 years ago

Our reliance on the standard library's JSON marshaller/unmarshaller is unnecessary (and expensive) as we have all the type information available during code generation. Consider the below benchmarks.

The second benchmark (BenchmarkPrototypeMarshaller) uses a custom JSON marshaller that builds the JSON string with a strings.Builder. It starts with a 512-byte buffer (more didn't improve things). However, this value might need to be tuned for optimal performance.

The win is very clear and is worth investigating further.

go test -benchmem -bench .
goos: windows
goarch: amd64
pkg: armdataboxedge
cpu: AMD Ryzen 7 2700X Eight-Core Processor
BenchmarkCurrentMarshaller-4               87075             12604 ns/op            3121 B/op         40 allocs/op
BenchmarkPrototypeMarshaller-4            546716              2150 ns/op            1920 B/op          7 allocs/op
PASS
ok      armdataboxedge  2.839s
var addon = armdataboxedge.Addon{
    Kind: to.Ptr(armdataboxedge.AddonTypeArcForKubernetes),
    ID:   to.Ptr("/some/kind/of/resource/that/should/be/a/long/string/longer/than/this/most/likely"),
    Name: to.Ptr("FooBarBaz"),
    SystemData: &armdataboxedge.SystemData{
        CreatedAt:          to.Ptr(time.Now()),
        CreatedBy:          to.Ptr("jhendrix"),
        CreatedByType:      to.Ptr(armdataboxedge.CreatedByTypeUser),
        LastModifiedAt:     to.Ptr(time.Now()),
        LastModifiedBy:     to.Ptr("jhendrix"),
        LastModifiedByType: to.Ptr(armdataboxedge.CreatedByTypeUser),
    },
    Type: to.Ptr("something/something"),
}

func BenchmarkCurrentMarshaller(b *testing.B) {
    for i := 0; i < b.N; i++ {
        if _, err := json.Marshal(addon); err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkPrototypeMarshaller(b *testing.B) {
    for i := 0; i < b.N; i++ {
        if _, err := addon.MarshalJSON2(); err != nil {
            b.Fatal(err)
        }
    }
}
jhendrixMSFT commented 2 years ago

Unfortunately, json.Decoder has some perf problems which prevents us from building performant, custom unmarshallers on top of it (see https://github.com/golang/go/issues/40128). For now, we'll just fix up the marshallers.

BenchmarkCurrentUnmarshaller-4             46093             26880 ns/op            4857 B/op         87 allocs/op
BenchmarkPrototypeUnmarshaller-4           44324             25808 ns/op            7200 B/op        286 allocs/op
github-actions[bot] commented 1 month ago

Hi @jhendrixMSFT, we deeply appreciate your input into this project. Regrettably, this issue has remained unresolved for over 2 years and inactive for 30 days, leading us to the decision to close it. We've implemented this policy to maintain the relevance of our issue queue and facilitate easier navigation for new contributors. If you still believe this topic requires attention, please feel free to create a new issue, referencing this one. Thank you for your understanding and ongoing support.