Goで高速にJSONを扱うライブラリeasyjsonとfastjson

(2018-10-24)

easyjson

easyjsonは以下のようなstructごとのコードを自動生成してReflectionなしで高速にJSON Marshal/Unmarshalできるようにするライブラリ。

$ go get -u github.com/mailru/easyjson/...
$ cat struct.go
package a

type Foo struct {
        A string `json:"a,omitempty"`
        B *Bar
}

type Bar struct {
        C []int          `json:"c"`
        D map[string]int `json:"d"`
}

$ easyjson -all struct.go
$ cat struct_easyjson.go
...
func easyjson9f2eff5fEncodeGithubComSambaizBenchjsonA(out *jwriter.Writer, in Foo) {
	out.RawByte('{')
	first := true
	_ = first
	if in.A != "" {
		const prefix string = ",\"a\":"
		if first {
			first = false
			out.RawString(prefix[1:])
		} else {
			out.RawString(prefix)
		}
		out.String(string(in.A))
	}
	{
		const prefix string = ",\"B\":"
		if first {
			first = false
			out.RawString(prefix[1:])
		} else {
			out.RawString(prefix)
		}
		if in.B == nil {
			out.RawString("null")
		} else {
			(*in.B).MarshalEasyJSON(out)
		}
	}
	out.RawByte('}')
}
...

標準のencoding/jsonと同じ様に使える。

x := a.Foo{
    B: &a.Bar{
        C: []int{1, 2, 3},
    },
}
b, err := easyjson.Marshal(x)
if err != nil {
    panic(err)
}
fmt.Println(string(b)) // {"B":{"c":[1,2,3],"d":null}}

fastjson

fastjsonはstructを介さず、ParseしてGetIntのような関数で取り出す。 そのため持ち回すとデータ形式が分からなくなるという問題はあるが、曖昧な型のJSONを扱う際はかえって使いやすいかもしれない。 MarshalToの引数には[]byteを渡せてnilを渡すとallocateされる。

var p fastjson.Parser
value, err := p.Parse(`{"a": {"b": 100, "c": null}}`)
fmt.Println(value.GetInt("a", "b")) // 100
fmt.Println(value.GetStringBytes("a", "b")) // []
fmt.Println(string(value.MarshalTo(nil))) // {"a":{"b":100,"c":null}}

ベンチマーク

簡単な例でベンチマークを取ってみる。

go testでベンチマークを取ってpprofで時間がかかっている箇所を調べる - sambaiz-net

package main

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/mailru/easyjson"
	"github.com/sambaiz/benchjson/a"
	"github.com/valyala/fastjson"
)

var target = a.Foo{
	A: strings.Repeat("ABCD", 100),
	B: &a.Bar{
		C: []int{1, 2, 3},
		D: map[string]int{
			"AAA": 100,
			"BBB": 200,
			"CCC": 300,
			"DDD": 400,
		},
	},
}

func targetBytes() []byte {
	dat, err := easyjson.Marshal(target)
	if err != nil {
		panic(err)
	}
	return dat
}

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

func BenchmarkEasyJSONMarshal(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, err := easyjson.Marshal(target)
		if err != nil {
			b.Error(err)
		}
	}
}

func BenchmarkFastJSONMarshalTo(b *testing.B) {
	dat := targetBytes()
	var p fastjson.Parser
	value, err := p.ParseBytes(dat)
	if err != nil {
		b.Error(err)
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		value.MarshalTo(nil)
	}
}

func BenchmarkEncodingJSONUnmarshal(b *testing.B) {
	dat := targetBytes()
	v := a.Foo{}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		err := json.Unmarshal(dat, &v)
		if err != nil {
			b.Error(err)
		}
	}
}

func BenchmarkEasyJSONUnmarshal(b *testing.B) {
	dat := targetBytes()
	v := a.Foo{}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		err := easyjson.Unmarshal(dat, &v)
		if err != nil {
			b.Error(err)
		}
	}
}

func BenchmarkFastJSONParse(b *testing.B) {
	dat := targetBytes()
	var p fastjson.Parser
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := p.ParseBytes(dat)
		if err != nil {
			b.Error(err)
		}
	}
}

確かにencoding/jsonと比べて格段に速い。適所で使っていきたい。

$ go test --bench .
goos: darwin
goarch: amd64
pkg: github.com/sambaiz/benchjson
BenchmarkEncodingJSONMarshal-4     	  300000	      4548 ns/op
BenchmarkEasyJSONMarshal-4         	 1000000	      1829 ns/op
BenchmarkFastJSONMarshalTo-4       	 3000000	       429 ns/op
BenchmarkEncodingJSONUnmarshal-4   	  300000	      5143 ns/op
BenchmarkEasyJSONUnmarshal-4       	 1000000	      1780 ns/op
BenchmarkFastJSONParse-4           	 5000000	       373 ns/op