探究|Go JSON 三方包哪家强?
阿里妹导读
引言
你真的了解 Go 标准库吗?
问题一:标准库可以反序列化普通的字符串吗?执行下面的代码会报错吗?
var s string
err := json.Unmarshal([]byte(`"Hello, world!"`), &s)
assert.NoError(t, err)
fmt.Println(s)
// 输出:
// Hello, world!
解:其实标准库解析不仅支持是对象、数组,同时也可以是字符串、数值、布尔值以及空值,但需要注意,上面字符串中的双引号不能缺,否则将不是一个合法的 json 序列,会返回错误。
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 与结构体中的定义不同,但忽略大小写后是相同的,那么依然能够为字段赋值。
为什么使用第三方库,标准库有哪些不足?
API 不够灵活:如没有提供按需加载机制等;
性能不太高:标准库大量使用反射获取值,首先 Go 的反射本身性能较差,较耗费 CPU 配置;其次频繁分配对象,也会带来内存分配和 GC 的开销;
三方库哪些家强?
热门的三方库有哪些? 内部实现原理是什么? 如何结合业务去选型?
库名 | encoder | decoder | compatible | star 数 (2023.04.19) | 社区维护性 |
StdLib(encoding/json)[2] | ✔️ | ✔️ | N/A | - | - |
FastJson(valyala/fastjson)[3] | ✔️ | ✔️ | ❌ | 1.9k | 较差 |
GJson(tidwall/gjson)[4] | ✔️ | ✔️ | ❌ | 12.1k | 较好 |
JsonParser(buger/jsonparser)[5] | ✔️ | ✔️ | ❌ | 5k | 较差 |
JsonIter(json-iterator/go)[6] | ✔️ | ✔️ | 部分兼容 | 12.1k | 较差 |
GoJson(goccy/go-json)[7] | ✔️ | ✔️ | ✔️ | 2.2k | 较好 |
EasyJson(mailru/easyjson)[8] | ✔️ | ✔️ | ❌ | 4.1k | 较差 |
Sonic(bytedance/sonic)[9] | ✔️ | ✔️ | ✔️ | 4.1k | 较好 |
评判标准
评判标准包含三个维度:
性能:内部实现原理是什么,是否使用反射机制;
稳定性:考虑到要投入生产使用,必须是一个较为稳定的三方库;
功能灵活性:是否支持 Unmarshal 到 map 或 struct,是否提供的一些定制化抽取的 API;
泛型(generic)编解码:json 没有对应的 schema,只能依据自描述语义将读取到的 value 解释为对应语言的运行时对象,例如:json object 转化为 Go map[string]interface{};
定型(binding)编解码:json 有对应的 schema,可以同时结合模型定义(Go struct)与 json 语法,将读取到的 value 绑定到对应的模型字段上去,同时完成数据解析与校验;
查找(get)& 修改(set):指定某种规则的查找路径(一般是 key 与 index 的集合),获取需要的那部分 json value 并处理。
功能评测
性能评测
Small[11](400B, 11 keys, 3 layers) Medium[12](13KB, 300+ key, 6 layers) Large[13](635KB, 10000+ key, 6 layers)
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)
}
})
}
具体的指标数据和统计结果,可参考benchmark_readme [14],总体结论如下:
常见优化思路有哪些?
定型编解码
动态函数组装
减少函数调用
code-gen
JIT
// 函数缓存
type cache struct {
functions map[*rtype]function
lock sync.Mutex
}
var (
global = func() [caches]*cache {
var caches [caches]*cache
for 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()
}
泛型编解码
数据反序列化的过程中,map 插入的开销很高; 在数据序列化过程中,map 遍历也远不如数组高效;
如果用一种与 json AST 更贴近的数据结构来描述,不但可以让转换过程更加简单,甚至可以实现 lazy-load 。
复用编码缓冲区
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 = data
bufPool.Put(buf)
Sonic 库为什么性能好?
原理调研
它的优化思路可以分成离线和在线:
离线场景:针对 Go 语言编译优化的不足,Sonic 核心计算函数使用 C 语言编写,使用 Clang 的深度优化编译选项,并开发了一套 asm2asm 工具,将完全优化的 x86 汇编翻译成 plan9 汇编,加载到 Golang 运行时,以供调用。
在线场景:通过自定义 AST,实现了按需加载;采用 JIT 技术在运行时对模式对应的操作码进行装配,以 Golang 函数的形式缓存到堆外内存。这样大大减少函数调用,同时也保证灵活性;
SIMD
asm2asm
JIT 汇编
RCU cache
自定义 AST
针对泛型编解码,基于 map 开销较大的考虑,Sonic 实现了更符合 json 结构的树形 AST;通过自定义的一种通用的泛型数据容器 sonic-ast 替代 Go interface,从而提升性能。
用 node {type, length, pointer} 表示任意一个 json 数据节点,并结合树与数组结构描述节点之间的层级关系。针对部分解析,考虑到解析和跳过之间的巨大速度差距,将 lazy-load 机制到 AST 解析器中,以一种更加自适应和高效的方式来减少多键查询的开销。
type Node struct {
v int64
t types.ValueType
p unsafe.Pointer
}
如何实现部分解析?
如何解决相同路径查找重复开销的问题?
函数调用优化
无栈内存管理:自己维护变量栈(内存池),避免 Go 函数栈扩展。
自动生成跳转表,加速 generic decoding 的分支跳转。
使用寄存器传参:尽量避免 memory load & store,将使用频繁的变量放到固定的寄存器上,如:json buffer、结构体指针;
重写函数调用:由于汇编函数不能内联到 Go 函数中,函数调用引入的开销甚至会抵消 SIMD 带来的性能提升,因此在 JIT 中重新实现了一组轻量级的函数调用(维护全局函数表+函数 offset)。
业务实践
适用场景
快速试用
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)
}
收益情况
使用事项
HTML Escape
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)
大型模式问题
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),
)
}
key 排序
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/encoder"
// Binding map only
m := map[string]interface{}{}
v, err := encoder.Encode(m, encoder.SortMapKeys)
// Or ast.Node.SortKeys() before marshal
var root := sonic.Get(JSON)
err := root.SortKeys()
暂不支持 arm 架构
总结
不太推荐使用 Jsoniter 库,原因在于: Go 1.8 之前,官方 Json 库的性能就收到多方诟病。不过随着 Go 版本的迭代,标准 json 库的性能也越来越高,Jsonter 的性能优势也越来越窄。如果希望有极致的性能,应该选择 Easyjson 等方案而不是 Jsoniter,而且 Jsoniter 近年已经不活跃了。
比较推荐使用 Sonic 库,因不论从性能和功能总体而言,Sonic 的表现的确很亮眼;此外,通过了解 Sonic 的内部实现原理,提供一种对于 cpu 密集型操作优化的“野路子”,即:通过编写高性能的 C 代码并经过优化编译后供 Golang 直接调用。其实并不新鲜,因为实际上 Go 源码中的一些 cpu 密集型操作底层就是编译成了汇编后使用的,如:crypto 和 math。
参考资料:
深入 Go 中各个高性能 JSON 解析库:https://www.luozhiyun.com/archives/535 Go 语言原生的 json 包有什么问题?如何更好地处理 JSON 数据?:https://cloud.tencent.com/developer/article/1820473 go-json#how-it-works:https://github.com/goccy/go-json#how-it-works sonic :基于 JIT 技术的开源全场景高性能 JSON 库 Introduction to Sonic:https://github.com/bytedance/sonic/blob/main/INTRODUCTION.md bytedance/sonic-readme:https://pkg.go.dev/github.com/bytedance/[email protected]#section-readme 为字节节省数十万核的 json 库 sonic:https://zhuanlan.zhihu.com/p/586050976
[1]https://pkg.go.dev/encoding/json?spm=ata.21736010.0.0.6e462b76wytEry
[2]https://pkg.go.dev/encoding/json?spm=ata.21736010.0.0.6e462b76wytEry
[3]https://github.com/valyala/fastjson?spm=ata.21736010.0.0.6e462b76wytEry
[4]https://github.com/tidwall/gjson?spm=ata.21736010.0.0.6e462b76wytEry
[5]https://github.com/buger/jsonparser?spm=ata.21736010.0.0.6e462b76wytEry
[6]https://github.com/json-iterator/go?spm=ata.21736010.0.0.6e462b76wytEry
[7]https://github.com/goccy/go-json?spm=ata.21736010.0.0.6e462b76wytEry
[8]https://github.com/mailru/easyjson?spm=ata.21736010.0.0.6e462b76wytEry
[9]https://github.com/bytedance/sonic?spm=ata.21736010.0.0.6e462b76wytEry
[10]https://github.com/zhao520a1a/go-base/blob/master/json/benchmark_test/bench.sh?spm=ata.21736010.0.0.6e462b76wytEry&file=bench.sh
[11]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/small.go?spm=ata.21736010.0.0.6e462b76wytEry&file=small.go
[12]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/medium.go?spm=ata.21736010.0.0.6e462b76wytEry&file=medium.go
[13]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/large.json?spm=ata.21736010.0.0.6e462b76wytEry&file=large.json
[14]https://github.com/zhao520a1a/go-base/blob/master/json/benchmark_test/README.md?spm=ata.21736010.0.0.6e462b76wytEry&file=README.md
[15]https://github.com/simdjson/simdjson?spm=ata.21736010.0.0.6e462b76wytEry
[16]https://github.com/minio/simdjson-go?spm=ata.21736010.0.0.6e462b76wytEry
[17]https://github.com/brahma-adshonor/gohook?spm=ata.21736010.0.0.6e462b76wytEry
[19]https://github.com/bytedance/sonic?spm=ata.21736010.0.0.6e462b76wytEry#compatibility
[20]https://github.com/bytedance/sonic/issues/172?spm=ata.21736010.0.0.6e462b76wytEry
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
微信扫码关注该文公众号作者