为字节节省数十万核的json库sonic

sonic是一个针对Golang的高性能JSON库,旨在解决服务中JSON操作的CPU开销问题。它通过热点操作编译成汇编、使用SIMD优化和循环展开等技术,实现了显著的性能提升。文章介绍了sonic的背景、快速试用方法、内部实现细节,包括编译优化、运行时汇编和懒加载技术,以及进一步的优化策略。sonic已在字节跳动内部广泛应用,节省了大量计算资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值