• go使用benchmark分析json库性能


    我们可选择的 json 库有很多,而之所以放弃官方的 json 库,多是出于性能考虑。不过,在选择采用某个 json 库之前,建议大家不要盲目相信性能评分,还是要深入自己业务场景,多出一些自己 benchmark 的评测结果。

    最重要的,还是要搞清楚,选择的 json 库究竟做了哪些优化,才使得性能有了较大提高,它的这些优化项是否符合我们的业务场景?

    fastjson

    主要分析下面2个json库,判断他们的性能差异

    ① github.com/valyala/fastjson
    ② github.com/bitly/go-simplejson

    假设我们要解码的字符串都是二维数组类型,业务需求是读取二维数组中的某个元素。二维数组的example如下,这样的编码方式主要为了节省存储空间,省去了存储 key 的额外开销。我们要读取内容中的 china 值,也就是第一个数组中的第二个元素:

    [["2", "china", "10", "55", "1", "text", "0", "0", "0", "11ms", "cpu"],[""]]
    
    • 1

    使用 fastjson 读取 china 的代码实现:

    func FastJson(str string) string {
    	json, _ := fastjson.Parse(str)
    	arrs, _ := json.Array()
    	for _, elem := range arrs {
    		sub, _ := elem.Array()
    		result, _ := sub[1].StringBytes()
    		return string(result)
    	}
    
    	return ""
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    使用simplejson读取china 的代码实现:

    func SimpleJson(str string) string {
    	json, _ := simplejson.NewJson([]byte(str))
    	arrs, _ := json.Array()
    	for _, elem := range arrs {
    		sub, _ := elem.([]interface{})
    		return sub[1].(string)
    	}
    
    	return ""
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    编写benchmark测试用例,分析两个方法的性能,测试用例如下:

    import "testing"
    
    var (
    	str   = `[["2", "china", "10", "55", "1", "text", "0", "0", "0", "11ms", "cpu"],[""]]`
    	china string
    )
    
    func BenchmarkFastJson(b *testing.B) {
    	for i := 0; i < b.N; i++ {
    		china = FastJson(str)
    	}
    }
    
    func BenchmarkSimpleJson(b *testing.B) {
    	for i := 0; i < b.N; i++ {
    		china = SimpleJson(str)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    每个方法执行5次,控制台查看benchmark的分析结果,粗略统计,fastjson是simplejson性能的2倍。通过 benchstat 我们还可以获得性能的分析结果

    go test -bench=. -benchmem -count=5 . | tee p.txt 
    
    • 1

    在这里插入图片描述
    在使用 benchstat之前需要先安装这个工具,它分析的数据源就是我们 bench 执行的输出结果,全部导出到了文件 p.txt中。

    go get -u golang.org/x/perf/cmd/benchstat
    
    • 1

    使用 benchstat 来查看性能的分析结果

    benchstat p.txt
    
    • 1

    通过分析结果,我们可以清楚的看到两个函数的性能差异,单次执行的耗时、申请内存次数、内存大小的比较

    在这里插入图片描述

    fastjson文档说,比原生的json快了 15x 的性能,这还远远没有达到呢。其实fastjson提供了sync.Pool的功能,我们在FastJson方法的基础上应用对象池来看看性能的变化。

    另外,我们要比较fastjson使用sync.Pool和不使用sync.Pool的两种方式。观察一下两者的性能差异。先将当前的go test 执行程序保存下来

    # 该例子中不需要保持,删除这段
    # go test -c 
    # 重命名 *.test 为 old.text
    
    • 1
    • 2
    • 3

    修改代码,fastjson使用sync.Pool的处理方式,将FastJson修改为如下,通过ParserPool声明一个对象池,然后每次执行解析的时候从池中获取一个parser对象,执行完成之后,放回去

    var fastjsonPool = fastjson.ParserPool{}
    
    func FastJson(str string) string {
    	parser := fastjsonPool.Get()
    	defer fastjsonPool.Put(parser)
    
    	json, _ := parser.Parse(str)
    	arrs, _ := json.Array()
    	for _, elem := range arrs {
    		sub, _ := elem.Array()
    		result, _ := sub[1].StringBytes()
    		return string(result)
    	}
    
    	return ""
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    我们将这次benchmark的执行结果保存到new.txt中,重新执行benchmark

     go test -bench=. -benchmem -count=5 . | tee new.txt 
    
    • 1

    通过 benchstat 比较两次的性能差异

    benchstat p.txt new.txt 
    
    • 1

    我们看FastJson old 和 new 的比较,性能提升了6倍

    在这里插入图片描述

    sonic

    字节 sonic 库给出了非常高的性能评分,针对 json 的性能优化,我们又多了一个选择。sonic 基上完全兼容了官方的 json 库,所以无需担心兼容问题。

    有一点需要指出:使用 sonic 在 marshal map 类型数据时,编码的顺序是不确定的。因为 map 的读取本身也是不保证顺序的,每次读取key/value 顺序可能都不相同。但神奇的是,官方json库的 map 序列化编码顺序是固定的,会按照 ASCII 码的升序排列。

    用下面的单测来验证一下,使用 sonic 编码后的 map 字段顺序是不固定的,而使用 json 编码后的 map 编码顺序是固定的。虽然 map 类型的编码顺序不那么重要,但还是挺奇怪的。

    // github.com/bytedance/sonic v1.9.2
    func TestSonicMap(t *testing.T) {
    	var meta = map[string]string{
    		"a": "a",
    		"b": "b",
    		"c": "c",
    		"d": "d",
    	}
    	a, _ := sonic.Marshal(meta)
    	b, _ := sonic.Marshal(meta)
    	t.Log(string(a), string(b))
    
    	aj, _ := json.Marshal(meta)
    	bj, _ := json.Marshal(meta)
    	t.Log(string(aj), string(bj))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    原来是官方的 json 库内部做了排序处理,可以查看 mapEncoder 的编码实现。源码中使用 sort.Slice 函数对 map 的键值做了升序处理。

    // Extract and sort the keys.
    sv := make([]reflectWithString, v.Len())
    mi := v.MapRange()
    for i := 0; mi.Next(); i++ {
    	sv[i].k = mi.Key()
    	sv[i].v = mi.Value()
    	if err := sv[i].resolve(); err != nil {
    		e.error(fmt.Errorf("json: encoding error for type %q: %q", v.Type().String(), err.Error()))
    	}
    }
    sort.Slice(sv, func(i, j int) bool { return sv[i].ks < sv[j].ks })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在看 snoic 用法的过程中,还看到了 反序列化结构中的字符串是不安全的 #191 Issue,挺有意思,不过我本地验证了一下,Issue 已经被修复了。关于 string 和 []byte 类型的强制内存转换,是性能和安全的权衡问题。

  • 相关阅读:
    【目标分类图像增强方法】
    peft模型微调--Prompt Tuning
    软件测试什么样的技术才能拿20K薪资?
    oracle 11g从DBF恢复数据
    Win10鼠标宏怎么设置?电脑设置鼠标宏的方法
    Hadoop生态系统官网、下载地址、文档
    Mybatis快速入门
    【23期】你知道Java面向对象设计的六大原则吗
    漂亮的bootstrap后台模板
    C#教程9:C#方法(Methods)
  • 原文地址:https://blog.csdn.net/f1520107395/article/details/127799167