tidwall / gjson

Get JSON values quickly - JSON parser for Go
MIT License
13.95k stars 841 forks source link

Benchmark shows that easyjson is faster #293

Closed KnBrBz closed 1 year ago

KnBrBz commented 1 year ago

With benchmark (auto generated code is omitted)

package gjsonvseasy

import (
    "testing"

    "github.com/mailru/easyjson"
    "github.com/tidwall/gjson"
)

type SampleJSON struct {
    S string `json:"s"`

    I int `json:"i"`

    B  float64 `json:"b"`
    A  float64 `json:"a"`
    M  float64 `json:"m"`
    MB float64 `json:"mb"`
    P  float64 `json:"p"`
    PB float64 `json:"pb"`

    T int64 `json:"t"`

    D  int `json:"d"`
    FI int `json:"fi"`

    QS bool `json:"qs"`
    TS bool `json:"ts"`
}

var bytes = []byte(`{"s": "BENCHTEST", "b": 57.654321, "a": 117.123456, "m": 10000000, "mb": 10000000, "p": 12.737993485790131, "pb": 12.739859708664888, "t": 1655299719123, "i": 9, "d": 5, "fi": 2, "qs": true, "ts": true}`)

var result SampleJSON

func BenchmarkEasyjson(b *testing.B) {
    var decodedData SampleJSON

    for i := 0; i < b.N; i++ {
        if err := easyjson.Unmarshal(bytes, &decodedData); err != nil {
            b.Fatal(err)
        }
    }
    result = decodedData
}

func BenchmarkGjsonCheck(b *testing.B) {
    var decodedData SampleJSON

    for i := 0; i < b.N; i++ {
        res := gjson.GetBytes(bytes, "s")
        if res.Type == gjson.Null {
            b.Fatal("`s` not found")
        }
        decodedData.S = res.String()

        res = gjson.GetBytes(bytes, "i")
        if res.Type == gjson.Null {
            b.Fatal("`i` not found")
        }
        decodedData.I = int(res.Int())

        res = gjson.GetBytes(bytes, "b")
        if res.Type == gjson.Null {
            b.Fatal("`b` not found")
        }
        decodedData.B = res.Float()

        res = gjson.GetBytes(bytes, "a")
        if res.Type == gjson.Null {
            b.Fatal("`a` not found")
        }
        decodedData.A = res.Float()

        res = gjson.GetBytes(bytes, "m")
        if res.Type == gjson.Null {
            b.Fatal("`m` not found")
        }
        decodedData.M = res.Float()

        res = gjson.GetBytes(bytes, "mb")
        if res.Type == gjson.Null {
            b.Fatal("`mb` not found")
        }
        decodedData.MB = res.Float()

        res = gjson.GetBytes(bytes, "p")
        if res.Type == gjson.Null {
            b.Fatal("`p` not found")
        }
        decodedData.P = res.Float()

        res = gjson.GetBytes(bytes, "pb")
        if res.Type == gjson.Null {
            b.Fatal("`pb` not found")
        }
        decodedData.PB = res.Float()

        res = gjson.GetBytes(bytes, "t")
        if res.Type == gjson.Null {
            b.Fatal("`t` not found")
        }
        decodedData.T = res.Int()

        res = gjson.GetBytes(bytes, "d")
        if res.Type == gjson.Null {
            b.Fatal("`d` not found")
        }
        decodedData.D = int(res.Int())

        res = gjson.GetBytes(bytes, "fi")
        if res.Type == gjson.Null {
            b.Fatal("`fi` not found")
        }
        decodedData.FI = int(res.Int())

        res = gjson.GetBytes(bytes, "qs")
        if res.Type == gjson.Null {
            b.Fatal("`qs` not found")
        }
        decodedData.QS = res.Bool()

        res = gjson.GetBytes(bytes, "ts")
        if res.Type == gjson.Null {
            b.Fatal("`ts` not found")
        }
        decodedData.TS = res.Bool()
    }
    result = decodedData
}

func BenchmarkGjsonNoCheck(b *testing.B) {
    var decodedData SampleJSON

    for i := 0; i < b.N; i++ {

        decodedData.S = gjson.GetBytes(bytes, "s").String()

        decodedData.I = int(gjson.GetBytes(bytes, "i").Int())

        decodedData.B = gjson.GetBytes(bytes, "b").Float()

        decodedData.A = gjson.GetBytes(bytes, "a").Float()

        decodedData.M = gjson.GetBytes(bytes, "m").Float()

        decodedData.MB = gjson.GetBytes(bytes, "mb").Float()

        decodedData.P = gjson.GetBytes(bytes, "p").Float()

        decodedData.PB = gjson.GetBytes(bytes, "pb").Float()

        decodedData.T = gjson.GetBytes(bytes, "t").Int()

        decodedData.D = int(gjson.GetBytes(bytes, "d").Int())

        decodedData.FI = int(gjson.GetBytes(bytes, "fi").Int())

        decodedData.QS = gjson.GetBytes(bytes, "qs").Bool()

        decodedData.TS = gjson.GetBytes(bytes, "ts").Bool()
    }
    result = decodedData
}

func BenchmarkGjsonMany(b *testing.B) {
    var decodedData SampleJSON

    paths := []string{"s", "i", "b", "a", "m", "mb", "p", "pb", "t", "d", "fi", "qs", "ts"}

    for i := 0; i < b.N; i++ {
        results := gjson.GetManyBytes(bytes, paths...)

        decodedData.S = results[0].String()

        decodedData.I = int(results[1].Int())

        decodedData.B = results[2].Float()

        decodedData.A = results[3].Float()

        decodedData.M = results[4].Float()

        decodedData.MB = results[5].Float()

        decodedData.P = results[6].Float()

        decodedData.PB = results[7].Float()

        decodedData.T = results[8].Int()

        decodedData.D = int(results[9].Int())

        decodedData.FI = int(results[10].Int())

        decodedData.QS = results[11].Bool()

        decodedData.TS = results[12].Bool()
    }
    result = decodedData
}

I am getting such results

goos: linux
goarch: amd64
pkg: snippets/gjson_vs_easy
cpu: 11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz
BenchmarkEasyjson-16             1502883               765.6 ns/op            16 B/op          1 allocs/op
BenchmarkGjsonCheck-16            418384              2445 ns/op             136 B/op         10 allocs/op
BenchmarkGjsonNoCheck-16          494484              2385 ns/op             136 B/op         10 allocs/op
BenchmarkGjsonMany-16             451920              2585 ns/op            1288 B/op         11 allocs/op

Maybe I am not using it properly?

tidwall commented 1 year ago

Can you provide the autogenerated code or the steps to autogenerate please?

KnBrBz commented 1 year ago

I am using this package Auto generated code

// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.

package gjsonvseasy

import (
    json "encoding/json"
    easyjson "github.com/mailru/easyjson"
    jlexer "github.com/mailru/easyjson/jlexer"
    jwriter "github.com/mailru/easyjson/jwriter"
)

// suppress unused package warning
var (
    _ *json.RawMessage
    _ *jlexer.Lexer
    _ *jwriter.Writer
    _ easyjson.Marshaler
)

func easyjsonC80ae7adDecodeGitlabOriginSunsoftProSnippetsGjsonVsEasy(in *jlexer.Lexer, out *SampleJSON) {
    isTopLevel := in.IsStart()
    if in.IsNull() {
        if isTopLevel {
            in.Consumed()
        }
        in.Skip()
        return
    }
    in.Delim('{')
    for !in.IsDelim('}') {
        key := in.UnsafeFieldName(false)
        in.WantColon()
        if in.IsNull() {
            in.Skip()
            in.WantComma()
            continue
        }
        switch key {
        case "s":
            out.S = string(in.String())
        case "i":
            out.I = int(in.Int())
        case "b":
            out.B = float64(in.Float64())
        case "a":
            out.A = float64(in.Float64())
        case "m":
            out.M = float64(in.Float64())
        case "mb":
            out.MB = float64(in.Float64())
        case "p":
            out.P = float64(in.Float64())
        case "pb":
            out.PB = float64(in.Float64())
        case "t":
            out.T = int64(in.Int64())
        case "d":
            out.D = int(in.Int())
        case "fi":
            out.FI = int(in.Int())
        case "qs":
            out.QS = bool(in.Bool())
        case "ts":
            out.TS = bool(in.Bool())
        default:
            in.SkipRecursive()
        }
        in.WantComma()
    }
    in.Delim('}')
    if isTopLevel {
        in.Consumed()
    }
}
func easyjsonC80ae7adEncodeGitlabOriginSunsoftProSnippetsGjsonVsEasy(out *jwriter.Writer, in SampleJSON) {
    out.RawByte('{')
    first := true
    _ = first
    {
        const prefix string = ",\"s\":"
        out.RawString(prefix[1:])
        out.String(string(in.S))
    }
    {
        const prefix string = ",\"i\":"
        out.RawString(prefix)
        out.Int(int(in.I))
    }
    {
        const prefix string = ",\"b\":"
        out.RawString(prefix)
        out.Float64(float64(in.B))
    }
    {
        const prefix string = ",\"a\":"
        out.RawString(prefix)
        out.Float64(float64(in.A))
    }
    {
        const prefix string = ",\"m\":"
        out.RawString(prefix)
        out.Float64(float64(in.M))
    }
    {
        const prefix string = ",\"mb\":"
        out.RawString(prefix)
        out.Float64(float64(in.MB))
    }
    {
        const prefix string = ",\"p\":"
        out.RawString(prefix)
        out.Float64(float64(in.P))
    }
    {
        const prefix string = ",\"pb\":"
        out.RawString(prefix)
        out.Float64(float64(in.PB))
    }
    {
        const prefix string = ",\"t\":"
        out.RawString(prefix)
        out.Int64(int64(in.T))
    }
    {
        const prefix string = ",\"d\":"
        out.RawString(prefix)
        out.Int(int(in.D))
    }
    {
        const prefix string = ",\"fi\":"
        out.RawString(prefix)
        out.Int(int(in.FI))
    }
    {
        const prefix string = ",\"qs\":"
        out.RawString(prefix)
        out.Bool(bool(in.QS))
    }
    {
        const prefix string = ",\"ts\":"
        out.RawString(prefix)
        out.Bool(bool(in.TS))
    }
    out.RawByte('}')
}

// MarshalJSON supports json.Marshaler interface
func (v SampleJSON) MarshalJSON() ([]byte, error) {
    w := jwriter.Writer{}
    easyjsonC80ae7adEncodeGitlabOriginSunsoftProSnippetsGjsonVsEasy(&w, v)
    return w.Buffer.BuildBytes(), w.Error
}

// MarshalEasyJSON supports easyjson.Marshaler interface
func (v SampleJSON) MarshalEasyJSON(w *jwriter.Writer) {
    easyjsonC80ae7adEncodeGitlabOriginSunsoftProSnippetsGjsonVsEasy(w, v)
}

// UnmarshalJSON supports json.Unmarshaler interface
func (v *SampleJSON) UnmarshalJSON(data []byte) error {
    r := jlexer.Lexer{Data: data}
    easyjsonC80ae7adDecodeGitlabOriginSunsoftProSnippetsGjsonVsEasy(&r, v)
    return r.Error()
}

// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *SampleJSON) UnmarshalEasyJSON(l *jlexer.Lexer) {
    easyjsonC80ae7adDecodeGitlabOriginSunsoftProSnippetsGjsonVsEasy(l, v)
}

It can be created via the command

easyjson -all model.go

if SampleJSON struct is defined in model.go

tidwall commented 1 year ago

Thanks for providing the autogenerated code.

I can confirm seeing similar results as you.

goos: darwin
goarch: arm64
pkg: example.com/gjsonvseasy
BenchmarkEasyjson-10             1560355           757.7 ns/op        16 B/op          1 allocs/op
BenchmarkGjsonCheck-10            471795          2521 ns/op         136 B/op         10 allocs/op
BenchmarkGjsonNoCheck-10          469798          2520 ns/op         136 B/op         10 allocs/op
BenchmarkGjsonMany-10             423038          2769 ns/op        1288 B/op         11 allocs/op

I don't recommend gjson for hydrating a structured model. GJSON is intended to be used for querying arbitrary json for specific values. It works by searching the json document as quickly as possible, and returning the found value right away.

Trying to fill a struct using GJSON, as in the example you show, means that every field needs a gjson.Get operation. Each Get operation starts from the beginning of the json document in search in search of the value. That's can be pretty expensive when dealing with lots of fields.

Maybe I am not using it properly?

They're probably a few things you can do differently.

func BenchmarkGjsonForEach(b *testing.B) {
    var decodedData SampleJSON
    str := string(bytes)
    for i := 0; i < b.N; i++ {
        gjson.Parse(str).ForEach(func(k, v gjson.Result) bool {
            switch k.String() {
            case "s":
                decodedData.S = v.String()
            case "i":
                decodedData.I = int(v.Int())
            case "b":
                decodedData.B = v.Float()
            case "a":
                decodedData.A = v.Float()
            case "m":
                decodedData.M = v.Float()
            case "mb":
                decodedData.MB = v.Float()
            case "p":
                decodedData.P = v.Float()
            case "pb":
                decodedData.PB = v.Float()
            case "t":
                decodedData.T = v.Int()
            case "d":
                decodedData.D = int(v.Int())
            case "fi":
                decodedData.FI = int(v.Int())
            case "qs":
                decodedData.QS = v.Bool()
            case "ts":
                decodedData.TS = v.Bool()
            }
            return true
        })
    }
    result = decodedData
}

But again, for this type of use case, I recommend just sticking with an unmarshaller library like easyjson.

Here are my results:


goos: darwin
goarch: arm64
BenchmarkEasyjson-10                 1549269           762.6 ns/op        16 B/op          1 allocs/op
BenchmarkGjsonCheck-10                471752          2526 ns/op         136 B/op         10 allocs/op
BenchmarkGjsonNoCheck-10              475734          2524 ns/op         136 B/op         10 allocs/op
BenchmarkGjsonMany-10                 421005          2752 ns/op        1288 B/op         11 allocs/op
BenchmarkGjsonCheckString-10          540081          2163 ns/op           0 B/op          0 allocs/op
BenchmarkGjsonNoCheckString-10        551227          2169 ns/op           0 B/op          0 allocs/op
BenchmarkGjsonForEach-10             1421556           846.6 ns/op         0 B/op          0 allocs/op
``
KnBrBz commented 1 year ago

Thank you for answer. Closing ticket.