版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | 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库 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左右差距)

发表评论

电子邮件地址不会被公开。

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