1 sonic产生的背景
为什么要优化?
json操作在服务中的cpu开销中占据相当的比重。根据字节所有服务的统计,json序列化和反序列化的开销接近10%,部分服务甚至达到40%。
golang现有的库中没有一个可以在全场景下保持优异的性能,即使是json-iterator,在泛型编解码、大数据量级场景下的性能也会下降。与其他语言相比,golang的各种json库速度都慢了很多,存在优化空间。
适用服务场景和降本收益
由于sonic优化的是json操作,所以在json操作的cpu开销占比较大的服务场景中收益会比较明显。比如网关、转发和入口服务等。
截止2022年1月份,sonic已应用于抖音,今日头条等服务,累计为字节节省了数十万核。下图为字节某服务使用sonic后高峰时段的cpu占用核数对比(图来源)。
2 快速试用sonic
2.1 较小侵入的使用方法
想要了解sonic会对自己的服务产生多大的性能提升,评估是否值得切换,可以下面的方式较小侵入地将当前使用的json库切换为sonic:使用github.com/brahma-adshonor/gohook,在main函数的入口处hook当前使用的json库函数为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)
}
从上面可以看到,hook是函数级的,因此可以具体验证具体函数的性能提升,也可以部分函数使用sonic(出于对某些函数的不信任、或者自己有性能更优异或更稳定的实现)。
关于gohook
github.com/brahma-adshonor/gohook的大概实现是向被hook的函数地址中写入跳转指令,直接跳转到新的函数地址。
需要注意的是,gohook未经过生产环境验证,建议仅测试使用。
2.2 注意事项
key排序
sonic在序列化时默认是不对key进行排序的。json的规范也与顺序无关,但若需要json是有序的,可以在序列化时选择排序的配置,大约会带来10%的性能损耗。排序方法如下:
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()
HTML Escape
默认不开启html Escape,因为会造成约15%的性能损耗,若需要开启,可以通过下面的方法:
import "github.com/bytedance/sonic"
v := map[string]string{
"&&":"<>"}
ret, err := Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":"\u003c\u003e"}}`
3 sonic的内部实现
golang为了提高编译速度,在编译时做的优化较少,而其他语言(如C语言使用clang或gcc编译)编译时可以获得深度的优化。sonic的核心技术点就是使用C语言编写热点操作,使用Clang的深度优化编译选项编译后供golang调用。
这里借鉴了 json-iterator 的组装各类型处理函数的实现,不同之处在于直接编译出来,减少了函数调用的开销。
simd-json也使用了simd,但是使用的是go的编译器,相比clang所做的优化更少。
为什么不使用cgo
使用cgo,可以直接用golang编译并调用C代码。import虚拟package C,并在注释中include C代码文件、声明C中实现的函数。
/*
#include "hello.c"
int SayHello();
double Sum();
*/
import "C"
即可在golang代码中通过C包名调用上面声明的C函数。编译命令与编译只包含go文件的编译命令相同(如go build main.go
)。
cgo也可以对C代码进行O3级别的优化。
与sonic相比,cgo的实现更加简便,也对代码进行了深度优化,似乎是一个更好的方案。但是cgo在调用c代码的时候引入了调度、切换线程栈等开销,会造成较大(有的场景中高达20多倍)的性能损耗。
golang也可以直接编译C语言代码,可以通过命令行go tool 6c -I $GOROOT/src/pkg/runtime -S add.c
进行编译(见参考文献5,未尝试过,待验证)。
3.1 热点操作编译成汇编
以序列化为例。序列化时有int转字符串、float转字符串等cpu消耗较高的操作,将这些函数使用C语言编写(native目录)。
3.1.1 代码级优化
3.1.1.1 SIMD
以查找前缀类空格字符个数的lspace的部分SSE代码为例,加载16字节(_mm_load_si128)到变量x,生成16个字节的类空格字符(_mm_set1_epi8)临时变量,比较16字节变量x和全是空格的临时变量(_mm_cmpeq_epi8),…
/* 16-byte loop */
while (likely(nb >= 16)) {
__m128i x = _mm_load_si128 ((const void *)sp);
__m128i a = _mm_cmpeq_epi8 (x, _mm_set1_epi8