版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | http://vearne.cc

1. 前言

众所周知, Golang标准库"encoding/json"的性能并不好,现在比较热的替代库是"json-iterator/go"
传送门: json-iterator/go
我们的日志打成json格式,然后做集中收集,为了提高性能用json-iterator替换encoding/json。打火焰图,却发现(JSON序列化日志)耗时占总时间的比重没有下降

2. 重新测试

难道json-iterator出的性能压测数据都是吹牛逼,只要自己又重新测试了一下,以下是测试代码 json_test.go

package tjson 

import (
    "testing"
    "encoding/json"
	jsoniter "github.com/json-iterator/go"
)

type Car struct {
	Name string
    Name1 string    
    Name2 string    
    Name3 string 
}

func BenchmarkStructJsoniter(b *testing.B) {
    c := Car{Name1:"xxxxxxxxxxxxx", Name2:"xxxxxxxxxxxxxxxxxxxx", Name3:"xxxxxxxxxxxxxxxxxx", Name: "buickxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
	for i := 0; i < b.N; i++ { //use b.N for looping
        jsoniter.Marshal(&c)
	}
}

func BenchmarkStructStd(b *testing.B) {
    c := Car{Name1:"xxxxxxxxxxxxx", Name2:"xxxxxxxxxxxxxxxxxxxx", Name3:"xxxxxxxxxxxxxxxxxx", Name: "buickxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
	for i := 0; i < b.N; i++ { //use b.N for looping
        json.Marshal(&c)
	}
}

func BenchmarkMapJsoniter(b *testing.B) {
    mymap := make(map[string]string, 10000)
    mymap["Name1"] = "xxxxxxxxxxxxx"
    mymap["Name2"] = "xxxxxxxxxxxxxxxxxxxx"
    mymap["Name3"] = "xxxxxxxxxxxxxxxxxx"
    mymap["Name"] = "buickxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    for i := 0; i < b.N; i++ { //use b.N for looping
        jsoniter.Marshal(&mymap)
    }
}

func BenchmarkMapStd(b *testing.B) {
    mymap := make(map[string]string, 10000)
    mymap["Name1"] = "xxxxxxxxxxxxx"
    mymap["Name2"] = "xxxxxxxxxxxxxxxxxxxx"
    mymap["Name3"] = "xxxxxxxxxxxxxxxxxx"
    mymap["Name"] = "buickxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    for i := 0; i < b.N; i++ { //use b.N for looping
        json.Marshal(&mymap)
    }
}

BenchMark脚本

go test -bench=. -benchmem

3. 结果

goos: darwin
goarch: amd64
BenchmarkStructJsoniter-4   	 2000000	       826 ns/op	     296 B/op	       2 allocs/op
BenchmarkStructStd-4        	 2000000	       894 ns/op	     496 B/op	       2 allocs/op
BenchmarkMapJsoniter-4      	  100000	     22197 ns/op	     430 B/op	       4 allocs/op
BenchmarkMapStd-4           	  100000	     24100 ns/op	    1109 B/op	      16 allocs/op

以下是对一个对象序列化1000次,所用的耗时

json库mapstruct
json-iterator22197 ns/op826 ns/op
encoding/json24100 ns/op894 ns/op

从输出结果我们可以看出

  1. 在对结构体对象做JSON序列化时,在Go1.8上json-iterator相比标准库有50%的性能提升, 而在Go 1.10 上性能几乎没有什么提升
  2. 对map对象做JSON序列化时,json-iterator和标准库相比几乎没有性能优势(这与前言中提到的替换了库,性能仍旧不能提升的情况一致)
  3. 对几乎同样大小的map对象和struct对象,无论是标准库还是json-iterator, 性能都几乎差2个数量级。
  4. 可见在编写Golang代码时要尽可能使用struct而不是map

为什么序列化map对象和struct对象会有如此大的性能差距呢

4. 简单的解释

带着疑问我简单的阅读的源码,似乎找到了答案 1)我们都知道JSON序列化是实际是一个递归过程。

type Car struct{
    //age int
    Name string
    Msg  string
}

比如要需要序列化一个Car对象,需要先反射得到Car中的每一个属性的名称和类型,然后在根据每一个属性的类型来决定是继续向下递归,还是调用相应的类型的encoder

2)在Golang中反射是一个非常耗时的操作。

3)json-iterator使用modern-go/reflect2来优化反射性能。然后就是通过大幅度减少反射操作,来提高速度。

详细代码 reflect_extension.go

// 保存 type -> Decoder
var typeDecoders = map[string]ValDecoder{}
// 保存 type/field -> Decoder
var fieldDecoders = map[string]ValDecoder{}
// 保存 type -> Encoder
var typeEncoders = map[string]ValEncoder{}
// 保存 type/field -> Encoder
var fieldEncoders = map[string]ValEncoder{}

对于struct而言,它的field只需要在在第一次操作时,反射一次name和type,并判断需要的Encoder或Decoder,而后续只需要获得field的name,无需获取field的type。

		fieldNames := calcFieldNames(field.Name(), tagParts[0], tag)
		// 注意这里
		fieldCacheKey := fmt.Sprintf("%s/%s", typ.String(), field.Name())
		decoder := fieldDecoders[fieldCacheKey]
		if decoder == nil {
			decoder = decoderOfType(ctx.append(field.Name()), field.Type())
		}
		encoder := fieldEncoders[fieldCacheKey]
		if encoder == nil {
			encoder = encoderOfType(ctx.append(field.Name()), field.Type())
		}

相对而言,因为map对象的key和value的name和type是不固定的,因此上面的优化没有办法应用在map上。所以我们看到测试结果中,对map对象的序列化,json-iterator库和标准库并没有显著的差异。

总结

json-iterator的api与标准库接近,很容易平滑的替换 注意只有使用struct才能获得显著的性能提升

PS: 在使用json-iterator的过程中,没有发现有json-iterator有内存泄露问题,可以放心使用,但在Go1.10以后性能与标准库差异不大,这个第三方库的意义已经不大了。


请我喝瓶饮料

微信支付码