Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://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库 map struct
json-iterator 22197 ns/op 826 ns/op
encoding/json 24100 ns/op 894 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以后性能与标准库差异不大,这个第三方库的意义已经不大了。


请我喝瓶饮料

微信支付码

5 对 “json-iterator 使用要注意的大坑”的想法;

  1. 我使用更加复杂的json格式,数据类型更多,嵌套更多,发现即使在1.10版本中,jsoniter的速度也还是比原生json快一倍左右

    1. jsoniter对struct序列化相比原生的库,肯定还是有提升的,只是不如在1.8的时候提升的那么大了。

  2. 我用 go1.11 测试发现,benchmark时虽然耗时小,到时内存分配多, 然后再生产环境实际数据跑发现性能比标准库慢了一倍。pprof发现耗时主要在内存分配上。

  3. 你这个序列化map压测有问题,只有4个key,却分配了10000的size。如果只分配4个size再压测会发现jsoniter还是有性能优势的(本地go1.12测试 2x左右差距)

    1. 在go.12 上测试,确实如你所说还是有2倍左右的性能差距的
      可能是我的case设计的不大恰当

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据