首页 前端知识 sonic :基于 JIT 技术的开源全场景高性能 JSON 库

sonic :基于 JIT 技术的开源全场景高性能 JSON 库

2024-06-19 08:06:39 前端知识 前端哥 27 532 我要收藏
  • 查找(get)& 修改(set):指定某种规则的查找路径(一般是 key 与 index 的集合),获取需要的那部分 JSON value 并处理。

其次,我们根据样本 JSON 的 key 数量和深度分为三个量级:

  • 小(small):400B,11 key,深度 3 层;

  • 中(medium):110KB,300+ key,深度 4 层(实际业务数据,其中有大量的嵌套 JSON string);

  • 大(large):550KB,10000+ key,深度 6 层。

测试结果如下:

de9e9797f5db5f66d60bd7618e3bd7d7.png

不同数据量级下 JSON 库性能表现

结果显示:目前这些 JSON 库均无法在各场景下都保持最优性能,即使是当前使用最广泛的第三方库 json-iterator,在泛型编解码、大数据量级场景下的性能也满足不了我们的需要

JSON 库的基准编解码性能固然重要,但是对不同场景的最优匹配更关键 —— 于是我们走上了自研 JSON 库的道路。

开源库 sonic 技术原理


由于 JSON 业务场景复杂,指望通过单一算法来优化并不现实。于是在设计 sonic 的过程中,我们借鉴了其他领域/语言的优化思想(不仅限于 JSON),将其融合到各个处理环节中。其中较为核心的技术有三块:JIT、lazy-loadSIMD

JIT

对于有 schema 的定型编解码场景而言,很多运算其实不需要在“运行时”执行。这里的“运行时”是指程序真正开始解析 JSON 数据的时间段。

举个例子,如果业务模型中确定了某个 JSON key 的值一定是布尔类型,那么我们就可以在序列化阶段直接输出这个对象对应的 JSON 值(‘true’或‘false’),并不需要再检查这个对象的具体类型。

sonic-JIT 的核心思想就是:将模型解释与数据处理逻辑分离,让前者在“编译期”固定下来

这种思想也存在于标准库和某些第三方 JSON 库,如 json-iterator 的函数组装模式:把 Go struct 拆分解释成一个个字段类型的编解码函数,然后组装并缓存为整个对象对应的编解码器(codec),运行时再加载出来处理 JSON。但是这种实现难以避免转化成大量 interface 和 function 调用栈,随着 JSON 数据量级的增长,function-call 开销也成倍放大。只有将模型解释逻辑真正编译出来,实现 stack-less 的执行体,才能最大化 schema 带来的性能收益。

业界实现方式目前主要有两种:代码生成 code-gen(或模版 template)和 即时编译 JIT。前者的优点是库开发者实现起来相对简单,缺点是增加业务代码的维护成本和局限性,无法做到秒级热更新——这也是代码生成方式的 JSON 库受众并不广泛的原因之一。JIT 则将编译过程移到了程序的加载(或首次解析)阶段,只需要提供 JSON schema 对应的结构体类型信息,就可以一次性编译生成对应的 codec 并高效执行。

sonic-JIT 大致过程如下:

4e0cb6b7f49b08b9b1999bb4d9ce60de.png

sonic-JIT 体系

  1. 初次运行时,基于 Go 反射来获取需要编译的 schema 信息;

  2. 结合 JSON 编解码算法生成一套自定义的中间代码 OP codes;

  3. 将 OP codes 翻译为 Plan9 汇编;

  4. 使用第三方库 golang-asm 将 Plan 9 转为机器码;

  5. 将生成的二进制码注入到内存 cache 中并封装为 go function;

  6. 后续解析,直接根据 type ID (rtype.hash)从 cache 中加载对应的 codec 处理 JSON。

从最终实现的结果来看,sonic-JIT 生成的 codec 性能不仅好于 json-iterator,甚至超过了代码生成方式的 easyjson(见后文“性能测试”章节)。这一方面跟底层文本处理算子的优化有关(见后文“SIMD & asm2asm”章节),另一方面来自于 sonic-JIT 能控制底层 CPU 指令,在运行时建立了一套独立高效的 ABI(Application Binary Interface)体系:

  • 将使用频繁的变量放到固定的寄存器上(如 JSON buffer、结构体指针),尽量避免 memory load & store;

  • 自己维护变量栈(内存池),避免 Go 函数栈扩展;

  • 自动生成跳转表,加速 generic decoding 的分支跳转;

  • 使用寄存器传递参数(当前 Go Assembly 并未支持,见“SIMD & asm2asm”章节)。

Lazy-load

对于大部分 Go JSON 库,泛型编解码是它们性能表现最差的场景之一,然而由于业务本身需要或业务开发者的选型不当,它往往也是被应用得最频繁的场景。

泛型编解码性能差仅仅是因为没有 schema 吗?其实不然。我们可以对比一下 C++ 的 JSON 库,如 rappidjson、simdjson,它们的解析方式都是泛型的,但性能仍然很好(simdjson 可达 2GB/s 以上)。标准库泛型解析性能差的根本原因在于它采用了 Go 原生泛型——interface(map[string]interface{})作为 JSON 的编解码对象

这其实是一种糟糕的选择:首先是数据反序列化的过程中,map 插入的开销很高;其次在数据序列化过程中,map 遍历也远不如数组高效。

回过头来看,JSON 本身就具有完整的自描述能力,如果我们用一种与 JSON AST 更贴近的数据结构来描述,不但可以让转换过程更加简单,甚至可以实现按需加载(lazy-load)——这便是 sonic-ast 的核心逻辑:它是一种 JSON 在 Go 中的编解码对象,用 node {type, length, pointer} 表示任意一个 JSON 数据节点,并结合树与数组结构描述节点之间的层级关系

d1eba71c3a30889103fbbfa396e0c052.png

sonic-ast 结构示意

sonic-ast 实现了一种有状态、可伸缩的 JSON 解析过程:当使用者 get 某个 key 时,sonic 采用 skip 计算来轻量化跳过要获取的 key 之前的 json 文本;对于该 key 之后的 JSON 节点,直接不做任何的解析处理;仅使用者真正需要的 key 才完全解析(转为某种 Go 原始类型)。由于节点转换相比解析 JSON 代价小得多,在并不需要完整数据的业务场景下收益相当可观。

虽然 skip 是一种轻量的文本解析(处理 JSON 控制字符“[”、“{”等),但是使用类似 gjson 这种纯粹的 JSON 查找库时,往往会有相同路径查找导致的重复开销。

针对该问题,sonic 在对于子节点 skip 处理过程增加了一个步骤,将跳过 JSON 的 key、起始位、结束位记录下来,分配一个 Raw-JSON 类型的节点保存下来,这样二次 skip 就可以直接基于节点的 offset 进行。同时 sonic-ast 支持了节点的更新、插入和序列化,甚至支持将任意 Go types 转为节点并保存下来。

换言之,sonic-ast 可以作为一种通用的泛型数据容器替代 Go interface,在协议转换、动态代理等服务场景有巨大潜力。

SIMD & asm2asm

无论是定型编解码场景还是泛型编解码场景,核心都离不开 JSON 文本的处理与计算。其中一些问题在业界已经有比较成熟高效的解决方案,如浮点数转字符串算法 Ryu,整数转字符串的查表法等,这些都被实现到 sonic 的底层文本算子中。

还有一些问题逻辑相对简单,但是可能会面对较大数量级的文本,如 JSON string 的 unquote\quote 处理、空白字符的跳过等。此时我们就需要某种技术手段来提升处理能力。SIMD 就是这样一种用于并行处理大规模数据的技术,目前大部分 CPU 已具备 SIMD 指令集(例如 Intel AVX),并且在 simdjson 中有比较成功的实践。

下面是一段 sonic 中 skip 空白字符的算法代码:

#if USE_AVX2

// 一次比较比较32个字符

while (likely(nb >= 32)) {

// vmovd 将单个字符转成YMM

__m256i x = _mm256_load_si256 ((const void *)sp);

// vpcmpeqb 比较字符,同时为了充分利用CPU 超标量特性使用4 倍循环

__m256i a = _mm256_cmpeq_epi8 (x, _mm256_set1_epi8(’ '));

__m256i b = _mm256_cmpeq_epi8 (x, _mm256_set1_epi8(‘\t’));

__m256i c = _mm256_cmpeq_epi8 (x, _mm256_set1_epi8(‘\n’));

__m256i d = _mm256_cmpeq_epi8 (x, _mm256_set1_epi8(‘\r’));

// vpor 融合4次结果

__m256i u = _mm256_or_si256   (a, b);

__m256i v = _mm256_or_si256   (c, d);

__m256i w = _mm256_or_si256   (u, v);

// vpmovmskb  将比较结果按位展示

if ((ms = _mm256_movemask_epi8(w)) != -1) {

_mm256_zeroupper();

// tzcnt 计算末尾零的个数N

return sp - ss + __builtin_ctzll(~(uint64_t)ms);

}

/* move to next block */

sp += 32;

nb -= 32;

}

/* clear upper half to avoid AVX-SSE transition penalty */

_mm256_zeroupper();

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

Android架构学习进阶是一条漫长而艰苦的道路,不能靠一时激情,更不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

上面分享的字节跳动公司2020年的面试真题解析大全,笔者还把一线互联网企业主流面试技术要点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

就先写到这,码字不易,写的很片面不好之处敬请指出,如果觉得有参考价值的朋友也可以关注一下我

①「Android面试真题解析大全」PDF完整高清版+②「Android面试知识体系」学习思维导图压缩包阅读下载,最后觉得有帮助、有需要的朋友可以点个赞

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

高清版+②「Android面试知识体系」学习思维导图压缩包阅读下载**,最后觉得有帮助、有需要的朋友可以点个赞

[外链图片转存中…(img-yvJ2vPxm-1712298276893)]

[外链图片转存中…(img-NqNqjqok-1712298276893)]

[外链图片转存中…(img-PmAmByfx-1712298276894)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
转载请注明出处或者链接地址:https://www.qianduange.cn//article/12826.html
标签
开源
评论
发布的文章

Markdown基础与进阶语法

2024-06-30 22:06:12

零基础 HTML 入门(详细)

2024-06-30 22:06:09

CSS3基本语法

2024-06-30 22:06:51

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!