1. sonic-rs 介绍
sonic-rs 是一个基于 SIMD 的高性能 Rust JSON 库,是 sonic JSON 库的 Rust 版本。
字节跳动 sonic 开源项目如今包含了不同语言的多个 JSON 库(如下)。其中,sonic-go 最先开源,使用了 JIT 和 SIMD 技术,sonic-cpp 使用了 C++ 模板和 SIMD 技术,这两个 JSON 库均已经在字节内部得到了较大规模的落地。在成本优化大背景下,为了帮助 Golang 业务迁移 Rust,优化 Rust JSON 性能,我们基于JSON 方面的优化经验和实践,用纯 Rust 语言开发了高性能的 JSON 库 sonic-rs。
-
sonic: github.com/bytedance/s... JSON 库)
-
sonic-cpp: github.com/bytedance/s... JSON 库)
-
sonic-rs: github.com/cloudwego/s... JSON 库)
sonic-rs 目前支持的 JSON 功能比较齐全,基本对齐了 serde-json 的相关功能,并且提供更加丰富的功能和更多的高性能接口。sonic-rs 的主要功能特点有:
-
基本兼容 Serde 生态,同时支持 Volo 中的
FastStr
类型 -
支持动态类型编解码和按需解析
-
支持
LazyVaue
,RawNumber
等类型 -
支持
UTF-8
校验和标准浮点数精度
在性能方面,我们基于 serde-rs 官方 benchmark(github.com/serde-rs/js...) 提供的 Rust 结构体和 JSON 数据,对 serde-json, simd-json 和 sonic-rs 在 Rust 结构体下的解析性能进行了测试,可以发现 sonic-rs 的性能是 simd-json 的 1.5~2 倍,是 serde-json 2 倍:
less
twitter/sonic_rs::from_slice_unchecked
time: [694.74 µs 707.83 µs 723.19 µs]
twitter/sonic_rs::from_slice
time: [796.44 µs 827.74 µs 861.30 µs]
twitter/simd_json::from_slice
time: [1.0615 ms 1.0872 ms 1.1153 ms]
twitter/serde_json::from_slice
time: [2.2659 ms 2.2895 ms 2.3167 ms]
twitter/serde_json::from_str
time: [1.3504 ms 1.3842 ms 1.4246 ms]
citm_catalog/sonic_rs::from_slice_unchecked
time: [1.2271 ms 1.2467 ms 1.2711 ms]
citm_catalog/sonic_rs::from_slice
time: [1.3344 ms 1.3671 ms 1.4050 ms]
citm_catalog/simd_json::from_slice
time: [2.0648 ms 2.0970 ms 2.1352 ms]
citm_catalog/serde_json::from_slice
time: [2.9391 ms 2.9870 ms 3.0481 ms]
citm_catalog/serde_json::from_str
time: [2.5736 ms 2.6079 ms 2.6518 ms]
canada/sonic_rs::from_slice_unchecked
time: [3.7779 ms 3.8059 ms 3.8368 ms]
canada/sonic_rs::from_slice
time: [3.9676 ms 4.0212 ms 4.0906 ms]
canada/simd_json::from_slice
time: [7.9582 ms 8.0932 ms 8.2541 ms]
canada/serde_json::from_slice
time: [9.2184 ms 9.3560 ms 9.5299 ms]
canada/serde_json::from_str
time: [9.0383 ms 9.2563 ms 9.5048 ms]
2. sonic-rs 优化实践
sonic-rs 的优化主要基于 SIMD,其中部分借鉴了其他 JSON 库 如 simd-json 的优化思路。SIMD (Single instruction, multiple data) 是一种并行优化技术,可以用一条指令,并行处理多个数据。如今大多数 CPU 已经支持了各种 SIMD 指令集。例如,x86_64 架构下的 SSE,AVX2,AVX512, aarch64 架构下的 neon 指令集等。使用 SIMD 指令优化之后,对于合适的任务,程序执行的指令数量会更少,因此性能会更好。
在整体设计上,sonic-rs 并没有采用 simd-json 那种二阶段解析的思路,主要将 SIMD 优化应用于JSON 解析和序列化中的热点,包括字符串序列化、按需解析和浮点数解析等。
2.1 SIMD 优化字符串序列化
字符串序列化是JSON 序列化的热点。序列化时,需要扫描字符串中的转义字符。对于较长的字符串,逐个字节判断转义字符的操作是比较耗时的,扫描转义字符非常适合使用 SIMD 来加速。
如果用 AVX2 指令来扫描转义字符,如下面代码所示。这段 SIMD 代码在 haswell 架构下面,开 O3 优化之后,其实只有六条 SIMD 指令,即 6 条 SIMD 指令可以一次性扫描 32个字节。相比较标量代码来说,大大减少了程序指令的数量,从而减少了程序的执行时间。
ini
static inline __m256i _mm256_find_quote(__m256i vv) {
__m256i e1 = _mm256_cmpgt_epi8 (vv, _mm256_set1_epi8(-1));
__m256i e2 = _mm256_cmpgt_epi8 (vv, _mm256_set1_epi8(31));
__m256i e3 = _mm256_cmpeq_epi8 (vv, _mm256_set1_epi8('"'));
__m256i e4 = _mm256_cmpeq_epi8 (vv, _mm256_set1_epi8('\\'));
__m256i r1 = _mm256_andnot_si256 (e2, e1);
__m256i r2 = _mm256_or_si256 (e3, e4);
__m256i rv = _mm256_or_si256 (r1, r2);
return rv;
}
2.2 SIMD 优化按需解析
很多业务场景只用到 JSON 中的部分字段,很适合按需解析,在解析时跳过不需要的 JSON 字段。在跳过 JSON 字段时,难点在于如何高效跳过 JSON 中的 object 和 array。
基于 JSON 中 object 和 array 括号必须匹配的语法规则,sonic-rs 使用 SIMD 实现了高效的括号匹配算法。先通过 SIMD 得到 json object 和 array 的 bitmap,然后通过计算括号的数量来跳过 object 和 array。当发现括号匹配时,就可以跳过该 object 或 array。
2.3 SIMD 优化浮点数解析
浮点数解析是 JSON 解析中的一个性能热点。在字节内部,我们发现 JSON 中大部分浮点数的尾数都比较长,也适合使用 SIMD 优化。如下图,对于一段长16个字节的浮点数尾数"1234342112345678":
-
先将这段字符串读取到向量寄存器里面,此时向量的每个数字还是 ASCII 码的值。
-
其次,用向量的减法,逐个字节减去 ASCII 码 '0' 得到 v1。这时。v1 里面的数字已经是十进制。
-
然后,继续对 v1 里面的各个数字用向量指令做两两乘加(高位乘以10 再加上低位),得到 v2。v2 里面的各个数已经是十进制的两位数。
-
以此类推,利用 SIMD 指令逐层累加,最终就得到 v16。v16 里面是一个 16 位数,即最终的尾数解析结果。
-
最后,我们再用向量指令把 v16转成 u64 类型。
整个解析过程,不用遍历浮点数尾数的每一个字符,就能完成浮点数尾数解析。
3. sonic-rs 现状和规划
sonic-rs 已经开源有三个多月,目前迭代到了 0.3 版本,已经支持 Rust stable 版本,并且支持了 aarch64 架构。sonic-rs 沉淀了一些使用文档,用以帮助各方面的开发者:
-
Golang 迁移 Rust 用户使用 sonic-rs: github.com/cloudwego/s...
-
Rust serde-json 用户迁移 sonic-rs: github.com/cloudwego/s...
-
性能优化细节:github.com/cloudwego/s...
后续,sonic-rs 会在性能,易用性和稳定性上面继续打磨,预期会支持对 Bytes/FastStr
等常见数据类型的零拷贝解析,支持运动时检测 SIMD 指令等,欢迎感兴趣的开发者一起加入我们。
项目地址
GitHub:github.com/cloudwego