Goで高速にJSONを扱うライブラリeasyjsonとfastjson
golangeasyjson
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