Sonic: Go语言的超级JSON库,解析与编码速度狂飙


Sonic: Go语言的超级JSON库,解析与编码速度狂飙

文章插图
Sonic是一款由字节跳动开发的一个全新的高性能、适用广泛的 JSON 库 。在设计上借鉴了多款JSON库,同时为了实现对标准库的真正插拔式替换,Sonic使用了 JIT[1] (即时编译)。介绍我们在日常开发中,常常会对JSON进行序列化和反序列化 。Golang提供了encoding/json包对JSON进行Marshal/Unmarshal操作 。但是在大规模数据场景下,该包的性能和开销确实会有点不够看 。在生产环境下,JSON 序列化和反序列化会被频繁的使用到 。在测试中,CPU使用率接近 10%,其中极端情况下超过 40% 。因此,JSON 库的性能是提高机器利用率的关键问题 。
Sonic是一款由字节跳动开发的一个全新的高性能、适用广泛的 JSON 库 。在设计上借鉴了多款JSON库,同时为了实现对标准库的真正插拔式替换,Sonic使用了 JIT[1] (即时编译)  。
Sonic的特色我们可以看出:Sonic是一个主打快的JSON库 。
  • 运行时对象绑定,无需代码生成
  • 完备的 JSON 操作 API
  • 快,更快,还要更快!
Sonic的设计
  1. 针对编解码动态汇编的函数调用开销,使用 JIT 技术在运行时组装与模式对应的字节码(汇编指令) ,最终将其以 Golang 函数的形式缓存在堆外内存上 。
  2. 针对大数据和小数据共存的实际场景,使用预处理判断(字符串大小、浮点数精度等)将 SIMD 与标量指令相结合,从而实现对实际情况的最佳适应 。
  3. 对于 Golang 语言编译优化的不足,使用 C/Clang 编写和编译核心计算函数,并且开发了一套 asm2asm[2] 工具,将经过充分优化的 x86 汇编代码转换为 Plan9 格式,最终加载到 Golang 运行时中 。
  4. 考虑到解析和跳过解析之间的速度差异很大, 惰性加载机制当然也在 AST 解析器中使用了,但以一种更具适应性和高效性的方式来降低多键查询的开销 。
在细节上,Sonic进行了一些进一步的优化:
  1. 由于 Golang 中的原生汇编函数不能被内联,发现其成本甚至超过了 C 编译器的优化所带来的改善 。所以在 JIT 中重新实现了一组轻量级的函数调用:
全局函数表+静态偏移量,用于调用指令
使用寄存器传递参数
  1. Sync.Map 一开始被用来缓存编解码器,但是对于准静态(读远多于写),元素较少(通常不足几十个)的场景,它的性能并不理想,所以使用开放寻址哈希和 RCU 技术重新实现了一个高性能且并发安全的缓存 。
安装使用当前我使用的go version是1.21 。
官方建议的版本:
  • Go 1.16~1.21
  • linux / macOS / windows(需要 Go1.17 以上)
  • Amd64 架构
# 下载sonic依赖$ go get Github.com/bytedance/sonic基本使用sonic提供了许多功能 。本文仅列举其中较为有特色的功能 。感兴趣的同学可以去看一下官方的examples
序列化/反序列化sonic的使用类似于标准包encoding/json包的使用.
func base() { m := map[string]interface{}{"name": "z3","age":20, }// sonic序列化 byt, err := sonic.Marshal(&m) if err != nil {log.Println(err) } fmt.Printf("json: %+vn", string(byt)) // sonic反序列化 um := make(map[string]interface{}) err = sonic.Unmarshal(byt, &um) if err != nil {log.Println(err) } fmt.Printf("unjson: %+vn", um)}// print// json: {"name":"z3","age":20}// unjson: map[age:20 name:z3]sonic还支持流式的输入输出
Sonic 支持解码 io.Reader 中输入的 json,或将对象编码为 json 后输出至 io.Writer,以处理多个值并减少内存消耗
func base() { m := map[string]interface{}{"name": "z3","age":20, } // 流式io编解码 // 编码 var encbuf bytes.Buffer enc := sonic.ConfigDefault.NewEncoder(&encbuf) if err := enc.Encode(m); err != nil {log.Fatal(err) } else {fmt.Printf("cutomize encoder: %+v", encbuf.String()) } // 解码 var decbuf bytes.Buffer decbuf.WriteString(encbuf.String()) clear(m) dec := sonic.ConfigDefault.NewDecoder(&decbuf) if err := dec.Decode(&m); err != nil {log.Fatal(err) } else {fmt.Printf("cutomize decoder: %+vn", m) }}// print// cutomize encoder: {"name":"z3","age":20}// cutomize decoder: map[age:20 name:z3]


推荐阅读