推广 热搜: csgo  vue  angelababy  2023  gps  新车  htc  落地  app  p2p 

探究|Go JSON 三方包哪家强?

   2023-08-09 网络整理佚名1170
核心提示:问题一:标准库可以反序列化普通的字符串吗?与结构体中的定义不同,但忽略大小写后是相同的,那么依然能够为字段赋值。做的事比标准库更糟糕,这是其跳过机制的副作用:相同路径查找导致的重复开销。因为这样实现会转化为大量的接口封装和函数调用栈。汇编是比较耗时的,会导致首次请求耗时较高,也可以预先生成好汇编后的字节码。已应用于抖音,今日头条等服务,累计为字节节省了数十万核。

阿里妹指南

本文作者从评价标准、功能评价、性能评价等多个方面分析了三方图书馆的优势,并给出了更为务实的建议。 (后台回复【Java单元测试实践】可以获得电子书)

介绍

为了方便朋友们的理解,我对文章中提到的术语和术语的解释进行了总结。 请放心阅读。 欢迎大家一起讨论交流!

你真的了解Go标准库吗?

问题1:标准库可以反序列化普通字符串吗? 下面的代码会报错吗?

var s stringerr := json.Unmarshal([]byte(`"Hello, world!"`), &s)assert.NoError(t, err)fmt.Println(s)// 输出:// Hello, world!

解决方案:其实标准库解析不仅支持对象和数组,还支持字符串、数字、布尔值和空值,但需要注意的是,上述字符串中的双引号不能缺失,否则会不是合法的json序列,返回错误。

问题2:如果结构体的json标签定义与key大小不一致,反序列化能否成功?

cert := struct {    Username string `json:"username"`    Password string `json:"password"`}{}err = json.Unmarshal([]byte(`{"UserName":"root","passWord":"123456"}`), &cert)if err != nil {    fmt.Println("err =", err)} else {    fmt.Println("username =", cert.Username)    fmt.Println("password =", cert.Password)}// 输出:// username = root// password = 123456

解决方案:如果遇到大小写问题,标准库会尝试转换大小写,即如果某个key与结构体中的定义不同,但忽略大小写后是一样的,仍然可以赋值到田野。

为什么要使用第三方库以及标准库的缺点是什么?

Go json标准库/json[1]已经提供了一个足够舒适的json处理工具,受到Go开发者的广泛好评,但是仍然存在两个问题:

基于以上考虑,业务会根据使用场景、降低成本和效益的要求,引入合适的第三方库。

哪些三方库强?

首先,思考从提问开始:

以下是我收集整理的一些开源第三方库。 也欢迎感兴趣的小伙伴一起交流补充!

库名

星级数 (2023.04.19)

社区维护

(/json)[2]

✔️

✔️

不适用

(/)[3]

✔️

✔️

1.9k

贫穷的

GJson (/gjson)[4]

✔️

✔️

12.1k

更好的

(错误/)[5]

✔️

✔️

5k

贫穷的

(json-/go)[6]

✔️

✔️

部分兼容

12.1k

贫穷的

(goccy/go-json)[7]

✔️

✔️

✔️

2.2k

更好的

(/)[8]

✔️

✔️

4.1k

贫穷的

索尼克(/索尼克)[9]

✔️

✔️

✔️

4.1k

更好的

评判标准

评判标准包括三个维度:

从功能划分上,根据主流json库API,分为三种使用方式:

功能评价

特征分析

绩效评估

以下是性能评估中使用的各个包的版本。 具体测试代码请参考[10]。

为什么GJson库没有被审核?

GJson在单键搜索的场景下有很大的优势。 这是因为它的搜索是通过lazy-load实现的,巧妙地跳过了传值,有效减少了很多不必要的解析,但实际上,跳过也是一种轻量级的解析,实际上是在处理json控制字符“[”,“{ “, ETC; 然而,在多键查找方面,Gjson 的表现比标准库更糟糕,这是其跳过机制的副作用:同路径查找的重复开销。 对于它的使用场景,简单地评价定性编解码器和通用编解码器对GJson来说是不公平的。

根据样本json的关键数量和深度,分为三个级别:

在评估过程中,不仅要注意区分通用编解码和定型编解码,还要考虑并发条件下的性能。 测试代码示例如下:

func BenchmarkEncoder_Generic_StdLib(b *testing.B) {    _, _ = json.Marshal(_GenericValue)    b.SetBytes(int64(len(TwitterJson)))    b.ResetTimer()for i := 0; i < b.N; i++ {        _, _ = json.Marshal(_GenericValue)    }}func BenchmarkEncoder_Binding_StdLib(b *testing.B) {    _, _ = json.Marshal(&_BindingValue)    b.SetBytes(int64(len(TwitterJson)))    b.ResetTimer()for i := 0; i < b.N; i++ {        _, _ = json.Marshal(&_BindingValue)    }}func BenchmarkEncoder_Parallel_Generic_StdLib(b *testing.B) {    _, _ = json.Marshal(_GenericValue)    b.SetBytes(int64(len(TwitterJson)))    b.ResetTimer()    b.RunParallel(func(pb *testing.PB) {for pb.Next() {            _, _ = json.Marshal(_GenericValue)        }    })}func BenchmarkEncoder_Parallel_Binding_StdLib(b *testing.B) {    _, _ = json.Marshal(&_BindingValue)    b.SetBytes(int64(len(TwitterJson)))    b.ResetTimer()    b.RunParallel(func(pb *testing.PB) {for pb.Next() {            _, _ = json.Marshal(&_BindingValue)        }    })}

具体指标数据及统计结果参见文献[14]。 总体结论如下:

常见的优化思路有哪些?

定型编解码器

对于一些刻板的编解码器场景,许多操作实际上并不需要在“运行时”执行。 例如,业务模型中确定某个json key的值必须是类型。 其实这个对象对应的json值(true/false)可以在序列化阶段直接输出,不需要检查这个对象的具体类型。 其核心思想是优化数据处理逻辑,将模型解释与数据处理逻辑分离,让前者提前固定下来,从而消除反射,提高性能。

动态功能组装

只需使用函数组装模式即可。 具体做法是将结构体解释为逐字段的编解码函数,然后组装缓存为整个对象对应的编解码器,然后在运行时加载处理json,减少了性能损失成本反射降至最低。

这个优化能一劳永逸吗?

一点也不。 因为这样的实现会转化为大量的接口封装和函数调用栈。 实际测试中会发现,随着json数据量级的增大,由于调用接口涉及到动态寻址,导致汇编函数无法内联,函数调用的性能很差(没有-by) -注册参数传递),而函数调用的开销也成倍增加。

减少函数调用

为了避免动态汇编的函数调用开销,目前业界主要有两种实现方式:code-gen(代码生成)和JIT(即时编译)。

代码生成

就用代码生成的思想吧。 这种实现方式对于库开发者来说实现起来比较简单,并且性能较高; 但随之而来的是模式依赖性和便利性的丧失,增加了业务代码的维护成本和局限性,并且无法实现同样是代码生成的秒级热更新。 之所以json库的受众并不广。

准时生产

即时编译将编译过程移至程序的加载或第一个解析阶段。 只需要提供json对应的结构体类型信息,就可以一次性编译生成对应的编解码器,通常会以函数的形式缓存在堆外内存中,方便后期高效执行。

// 函数缓存type cache struct {  functions map[*rtype]functionlock      sync.Mutex}var (global = func() [caches]*cache {var caches [caches]*cachefor idx := range caches {      caches[idx] = &cache{functions: make(map[*rtype]function, 4)}    }return caches  }())func load(typ *rtype) (function, bool) {do, ok := global[uintptr(unsafe.Pointer(typ))%caches].functions[typ]return do, ok}func save(typ *rtype, do function) {cache := global[uintptr(unsafe.Pointer(typ))%caches]  cache.lock.Lock()  cache.functions[typ] = do  cache.lock.Unlock()}

通用编解码器

其实通用编解码性能差不仅仅是因为没有,因为你可以对比一下C++的json库,比如[15],它的解析方式都是通用的,但是性能还是很好的;

Go标准库的通用解析性能较差,因为它使用map[]{}作为json的编解码对象。 这实际上是一个糟糕的选择,原因如下:

如果使用更接近json AST的数据结构来描述,不仅可以简化转换过程,甚至可以实现lazy-load。

多路编码缓冲器

通过使用sync.Pool重用之前编码的缓冲区,可以有效减少内存分配的次数。

type buffer struct {    data []byte}var bufPool = sync.Pool{    New: func() interface{} {return &buffer{data: make([]byte, 0, 1024)}    },}// 复用缓冲区buf := bufPool.Get().(*buffer)data:= encode(buf.data)newBuf := make([]byte, len(data))copy(newBuf, buf)buf.data = databufPool.Put(buf)

Sonic 库为什么性能好?

原理研究

Sonic基于汇编开发,充分利用矢量化(SIMD)指令、优化内存布局、按需解析等关键技术,极大提升了序列化和反序列化的性能。

其优化思路可以分为离线和在线:

为什么不使用CGO?

虽然使用CGO实现起来比较容易,但是CGO在调用c代码时引入了调度、切换线程栈等开销,会造成很大(某些场景高达20倍)的性能损失,并且无法深度优化代码。

单指令多数据流

什么是 SIMD?

SIMD(--Multi-Data Data )是一种利用一个控制器控制多个处理器,同时对一组数据中的每个数据进行相同操作,从而实现空间并行的方法。技术。 例如:X86的SSE或AVX2指令集,以及ARM的NEON指令集。 它作为一组特殊的 CPU 指令用于并行处理矢量数据。 目前大多数CPU都支持,广泛应用于图像处理和大数据计算,当然在json处理中也很有用。

SIMD在json处理中解决什么问题?

用于json文本的处理和计算。 其中一些问题业界已经有了比较成熟、高效的解决方案,比如浮点数转字符串的算法Ryu、整数转字符串的查表方法等; 对于有些问题,逻辑比较简单,但是可能面临大规模文本,比如json的\quote处理、空白字符跳过等,还需要一些技术手段来提高处理能力,而SIMD是一种并行的技术大规模数据的处理。 -go[16]在大型json场景(>100KB)很有竞争力。 然而,对于一些极小或不规则的字符串,SIMD所需的额外加载操作会导致性能下降。 因此,针对大数据和小数据并存的实际场景,使用预设的条件来判断(字符串大小、浮点精度等),结合SIMD和标量指令,以达到最佳的适应性。

为了提高执行效率,Sonic中的一些关键计算函数采用C语言编写,并使用Clang编译; 然而,由于 Clang 编译 x86 程序集,因此它编译 plan9 程序集; 如何嵌入优化后的程序集成为一个问题,因此为了在Byte中调用Clang编译的程序集,开发了一个内部工具tools/来将x86程序集转换为plan9。

即时编译

Sonic借鉴了组装各类处理函数的实现方式。 针对编解码器动态组装的函数调用开销,采用JIT技术在运行时组装模式对应的操作码(asm),最终以函数的形式缓存在堆外内存中。 由于编译后的编解码函数是集成函数,因此在保证灵活性的同时可以大大减少函数调用; 对于已知json对应结构的业务场景,在线JIT组装比较耗时,会导致耗时较高,也可以提前生成组装后的字节码。

RCU缓存

为了提高Codec Cache的加载速度,可以将每个结构体对应的序列化/反序列化字节码缓存起来,以后直接调用,以减少运行时汇编操作的执行次数(缓存足够大时只需执行一次)。 Sync.Map最初是用来缓存编解码器的,但是对于准静态(读多于写)和元素较少(通常不超过几十个)的场景,其性能并不理想,所以使用“”开放寻址哈希+RCU技术”重新实现了高性能且同时安全的缓存。

定制 AST

对于通用编解码,基于map开销较高的考虑,Sonic实现了更符合json结构的树状AST; 它用自定义通用通用数据容器 sonic-ast 取代了 Go,从而提高了性能。

使用node {type, , }来表示任意json数据节点,结合树和数组结构来描述节点之间的层次关系。 对于部分解析,考虑到解析和跳过之间巨大的速度差距,在 AST 解析器中加入了延迟加载机制,以更加自适应和高效的方式减少多键查询的开销。

type Node struct {v int64t types.ValueTypep unsafe.Pointer}

如何实现部分解析?

sonic-ast 实现了一个有状态的、可扩展的 json 解析过程。 当用户获取到key时,使用skip计算,轻轻跳过要获取的key之前的json文本; 对于key后面的json节点,不直接进行解析过程; 只保存用户真正需要的密钥。 完全解析了。

如何解决同一路径查找重复开销的问题?

子节点的skip处理过程增加了一步,记录跳过的json的key、起始位、结束位,并指定一个Raw-JSON类型的节点保存,这样第二次skip就可以直接根据Node ,解决了相同路径查找带来的重复开销问题。 同时,sonic-ast支持更新、插入和序列化节点,甚至支持将任何Go类型转换为节点并保存。

函数调用优化

商业实践

适用场景

由于Sonic对json操作进行了优化,因此在json操作的cpu开销占比较大的业务场景中,好处会更加明显。 如网关、转发和入口服务等。

快速试用

评估切换是否值得,以验证 Sonic 将以较少侵入的方式为您的服务带来性能改进。 推荐使用-/[17]工具库。 内部实现是向被hook的函数地址写入跳转指令,直接跳转到新的函数地址。

使用方法:在main函数入口处,hook当前使用的json库函数作为Sonic中的等效函数。 hook是在函数层面,因此可以详细验证具体函数的性能提升; 当人们不信任某些功能,或者他们有更好的性能或更稳定的实现时,他们也可以使用Sonic来实现某些功能。 但需要注意的是,尚未在生产环境中进行验证,建议仅用于测试。 请记住,在线更改时,必须满足“可监控”、“可灰化”、“可回滚”三个原则。

import "github.com/brahma-adshonor/gohook"func main() {// 在main函数的入口hook当前使用的json库(如encoding/json)    gohook.Hook(json.Marshal, sonic.Marshal, nil)    gohook.Hook(json.Unmarshal, sonic.Unmarshal, nil)}

收益

截至2022年1月,Sonic已应用于抖音、今日头条等服务,节省数十万核字节。 下图是使用Sonic(sonic:基于JIT技术的开源全场景高性能JSON库)后,一个Byte服务在高峰时段占用CPU核数的对比。 在生产环境中,Sonic也验证了良好的效益,高峰时段使用的核心数减少了近三分之一:

同时,我们将某在线服务的HTTP查询接口的JSON序列化从标准库切换到Sonic库后,在相同QPS水平下CPU利用率下降了近3个点,效果还不错。

使用说明HTML

标准库中默认启用了html,但Sonic由于性能损失而没有默认启用。

func TestEncode(t *testing.T) {  data:= map[string]string{"&&": "<>"}// 标准库var w1 = bytes.NewBuffer(nil)  enc1 := json.NewEncoder(w1)  err := enc1.Encode(data)  assert.NoError(t, err)// Sonic 库var w2 = bytes.NewBuffer(nil)  enc2 := encoder.NewStreamEncoder(w2)  err = enc2.Encode(data)  assert.NoError(t, err)  fmt.Printf("%v%v", w1.String(), w2.String())}// 运行结果:{"\u0026\u0026":"\u003c\u003e"}{"&&":"<>"}

如果需要,可以通过以下方法启用:

import "github.com/bytedance/sonic/encoder"v := map[string]string{"&&":"<>"}ret, err := encoder.Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":" \u003e"}}`enc := encoder.NewStreamEncoder(w)enc.SetEscapeHTML(true)err := enc.Encode(obj)

大模式问题

由于 Sonic 使用 -asm 作为 JIT 汇编器,不太适合运行时编译,因此第一次运行大型模式可能会导致请求超时甚至处理 OOM。 为了获得更好的稳定性,对于大模式或内存紧张的应用程序,建议在 ()/() 之前使用 ()。

import ("reflect""github.com/bytedance/sonic""github.com/bytedance/sonic/option")func init() {var v HugeStruct// For most large types (nesting depth <= option.DefaultMaxInlineDepth)    err := sonic.Pretouch(reflect.TypeOf(v))// with more CompileOption...    err := sonic.Pretouch(reflect.TypeOf(v),// If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),// you can set compile recursive loops in Pretouch for better stability in JIT.        option.WithCompileRecursiveDepth(loop),// For a large nested struct, try to set a smaller depth to reduce compiling time.        option.WithCompileMaxInlineDepth(depth),    )}

按键排序

默认情况下,Sonic 在序列化时不对键进行排序。 json的规范与顺序无关,但如果需要json有序,可以在序列化时选择排序配置,这会带来10%的性能损失。 排序方法如下:

import "github.com/bytedance/sonic"import "github.com/bytedance/sonic/encoder"// Binding map onlym := map[string]interface{}{}v, err := encoder.Encode(m, encoder.SortMapKeys)// Or ast.Node.SortKeys() before marshalvar root := sonic.Get(JSON)err := root.SortKeys()

目前不支持arm架构

现象:Mac M1编译失败。 解决方法请参考sonic-[19]。 编译时添加以下参数:=amd64可以解决编译失败的问题,但仍然无法支持本地Debug操作;

官方在[20]中表示,由于内部实现原因,这个问题确实很难解决,但仍有望在Sonic V2的大版本中得到支持。

总结

综上所述,业务选型需要根据具体情况、业务使用场景以及不同领域的发展趋势进行,综合考虑各种因素。 最适合的生意就是最好的! 例如:如果业务只是简单的解析http请求返回的json字符串的一些字段,并且字段都已经确定,并且偶尔需要搜索功能,那么Gjson是一个不错的选择。

以下是一些个人观点,仅供参考:

参考:

[1]

[2]

[3]

[4]

[5]

[6]

[7]

[8]

[9]

[10]

[11]

[12]

[13]

[14]

[15]

[16]

[17]

[19]#

[20]

阿里云开发者社区,千万开发者的选择

阿里云开发者社区,拥有百万优质技术内容、数千门免费系统课程、丰富的体验场景、活跃的社区活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。

 
反对 0举报 0 收藏 0 打赏 0评论 0
 
更多>同类资讯
推荐图文
推荐资讯
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报
Powered By DESTOON