mm换算cm(mm换算成英寸)-9游会

仓库:https://github.com/bytedance/sonic

sonic是字节跳动的一个开源golang json库。基于即时编译和单指令多数据技术,大大提高了围棋程序的json编译性能。同时,结合lazy-load设计思想,也为不同的业务场景打造了全面高效的api。

自2021年7月发布以来,sonic已被tik tok、今日头条和其他企业采用,为字节跳动节省了数十万个cpu内核。

json(jascript object notation)因其简洁的语法和灵活的自描述能力,被广泛应用于各种互联网服务中。但json本质上是文本协议,没有类似protobuf的强制模型约束(schema),所以编解码效率往往很低。再加上一些业务开发者对json库的选择和使用不当,服务性能会急剧恶化。

在字节跳动,我们也遇到了上述问题。根据公司cpu占top 50业务的性能分析数据,json编解码成本一般接近10%,单个业务占比甚至超过40%。提高json库的性能非常重要。因此,我们对业界现有的go json库进行了一些评测测试。

首先,根据主流的json库api,我们把它们的用法分为三种:

泛型(generic)编解码:json 没有对应的 schema,只能依据自描述语义将读取到的 value 解释为对应语言的运行时对象,例如:json object 转化为 go map[string]interface{};定型(binding)编解码:json 有对应的 schema,可以同时结合模型定义(go struct)与 json 语法,将读取到的 value 绑定到对应的模型字段上去,同时完成数据解析与校验;查找(get)& 修改(set):指定某种规则的查找路径(一般是 key 与 index 的集合),获取需要的那部分 json value 并处理。其次,我们根据样本 json 的 key 数量和深度分为三个量级:小(all):400b,11 key,深度 3 层;中(medium):110kb,300 key,深度 4 层(实际业务数据,其中有大量的嵌套 json string);大(large):550kb,10000 key,深度 6 层。

测试结果如下:

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

不同数据级别下json库的性能

结果表明,目前这些json库并不能在所有场景下都保持最佳性能。即使是应用最广泛的第三方库json-iterator,在通用编解码和大规模场景下也无法满足我们的需求。

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

由于json业务场景的复杂性,期望通过单一算法进行优化是不现实的。所以在设计sonic的过程中,我们借鉴了其他领域/语言(不限于json)的优化思路,并将其融入到所有处理环节中。有三种核心技术:jit、延迟加载和simd。

对于使用模式的定型编码和解码场景,许多操作不需要在运行时执行。这里的“运行时”是指程序实际开始解析json数据的时间段。

例如,如果在业务模型中确定一个json键值必须是布尔类型,那么我们可以在序列化阶段直接输出这个对象对应的json值(‘ true ‘或’ false ‘),而不需要检查这个对象的具体类型。

sonic-jit的核心思想是将模型解释与数据处理逻辑分离,使前者固定在“编译时”。

这种思想也存在于标准库和一些第三方json库中,比如json-iterator的函数组装模式:将go struct解释为每个字段类型的编解码器函数,然后组装缓存为整个对象对应的编解码器,然后在运行时加载处理json。然而,这种实现很难避免被转换成大量的接口和函数调用栈。随着json数据的增加,函数调用的成本成倍增加。只有真正编译模型解释逻辑,实现无栈执行器,模式带来的性能收益才能最大化。

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

sonic-jit的一般过程如下:

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

音速jit系统

初次运行时,基于 go 反射来获取需要编译的 schema 信息;结合 json 编解码算法生成一套自定义的中间代码 op codes;将 op codes 翻译为 plan9 汇编;使用第三方库 golang-a 将 plan 9 转为机器码;将生成的二进制码注入到内存 cache 中并封装为 go function;后续解析,直接根据 type id (rtype.hash)从 cache 中加载对应的 codec 处理 json。

从最终实现的结果来看,sonic-jit生成的编解码器性能不仅优于json-iterator,甚至超过了easyjson(详见后面的“性能测试”)。这一方面与底层文本处理操作符的优化有关(见下文“simd & a2a”一章),另一方面sonic-jit可以控制底层cpu指令,在运行时建立独立高效的abi(应用二进制接口)系统:

将使用频繁的变量放到固定的寄存器上(如 json buffer、结构体指针),尽量避免 memory load & store;自己维护变量栈(内存池),避免 go 函数栈扩展;自动生成跳转表,加速 generic decoding 的分支跳转;使用寄存器传递参数(当前 go assembly 并未支持,见“simd & a2a”章节)。

对于大多数go json库来说,通用编解码器是性能最差的场景之一。但由于业务本身的需要或者业务开发者选择不当,往往是最常使用的场景。

通用编解码器性能差只是因为没有图式吗?其实不是,我们可以对比c 的json库,比如rappidjson和simdjson。他们的解析方法一般,但是性能还是很不错的(simdjson可以达到2gb/s以上)。标准库泛型解析性能差的基本原因是它使用go native generics-interface(map[string]interface { })作为json的编解码对象。

其实这是一个不好的选择:首先,在数据反序列化的过程中,地图插入的成本非常高;其次,在数据序列化的过程中,映射遍历的效率远不如数组遍历。

回过头来看,json本身就有完整的自我描述能力。如果用一种更接近json ast的数据结构来描述,不仅可以使转换过程更简单,还可以实现lazy-load——这是sonic-ast的核心逻辑:它是go中的一个json codec对象,用node {type,length,pointer}表示任意json数据节点,结合树和数组结构描述节点之间的层次关系。

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

sonic-ast的结构示意图

sonic-ast实现了一个有状态的、可扩展的json解析过程:当用户获取一个密钥时,sonic使用skip calculation来淡化待获取密钥前的json文本;对于key之后的json节点,直接不做解析;只有用户真正需要的键才被完全解析(转换成某种go原语类型)。因为节点转换的成本远低于解析json的成本,所以在不需要完整数据的业务场景中,好处是相当可观的。

虽然skip是轻量级的文本解析(处理json控制字符“[”、“{”等。),在使用gjson这样的纯json查找库时,往往会出现相同路径查找导致的重复开销。

为了解决这个问题,sonic在子节点的skip处理中增加了一个步骤,记录被skip的json的key、start位和end位,并分配一个raw-json类型的节点来保存,这样就可以基于节点的偏移量直接进行二次skip。同时,sonic-ast支持节点的更新、插入和序列化,甚至支持将任何go类型变成节点并保存。

换句话说,sonic-ast可以作为一个通用的数据容器来代替go接口,在协议转换、动态代理等服务场景中有很大的潜力。

无论是定型编解码场景,还是通用编解码场景,核心都离不开对json文本的处理和计算。对于其中的一些问题,业界已经有成熟高效的j9九游会真人游戏第一品牌的解决方案,比如浮点数转字符串算法ryu、整数转字符串查找法等。,这些都是在sonic的底层文本操作符中实现的。

还有一些问题在逻辑上相对简单,但可能面对更大数量级的文本,比如json字符串的处理,跳过空白字符等。这时候就需要一些技术手段来提高处理能力。simd就是这样一种并行处理大规模数据的技术。目前,大多数cpu已经拥有simd指令集(如英特尔x),并且已经在simdjson中成功实践。

以下是sonic中skip 空白字符的算法代码:

#if use_x2 // 一次比较比较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 oid x-sse transition penalty */ _mm256_zeroupper();#endif

strnchr()在sonic中的实现(simd部分)

开发者会发现,这段代码其实是用c语言写的——其实sonic中大部分的文本处理功能都是用c语言实现的:一方面,simd指令集用c语言封装得很好,更容易实现;另一方面,这些c代码可以通过clang编译,充分享受其编译优化带来的提升。所以我们开发了一套将x86汇编转换为plan9汇编的工具a2a,通过go汇编机制将clang输出的汇编静态嵌入sonic。同时,在jit生成的编解码器中,我们利用a工具计算的c函数的pc值直接调用调用指令进行跳转,从而绕过go汇编不能注册参数的限制,挤压最后的cpu性能。

除了上面提到的技术,sonic内部还有很多细致的优化,比如用rcu代替sync。map提高编解码缓存的加载速度,使用内存池减少编码缓冲区的内存分配等等。由于这里篇幅有限,就不详细介绍了。有兴趣的同学可以自行搜索阅读sonic源代码。

我们在上一篇文章中测试了不同的测试场景,结果如下:

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

小数据(400b,11个键,3层深度)

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

中等数据(110kb,300 密钥,4层深度)

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

大数据(550kb,10000多个密钥,6层深度)

可以看出,sonic在几乎所有场景下都是领先的(sonic-ast直接使用go汇编导入的c函数,导致在小数据集下性能有所损失)

平均编码性能较 json-iterator 提升 240% ,平均解码性能较 json-iterator 提升 110% ;单 key 修改能力较 sjson 提升 75% 。

而且在生产环境中,sonic也验证了良好的回报,服务高峰期占用的内核数量减少了近三分之一:

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

sonic上线前后一个字节服务的cpu使用率(内核数)比较

由于底层是基于汇编开发的,sonic目前只支持amd64架构下的darwin/linux平台,未来会逐步扩展到其他操作系统和架构。此外,我们还考虑将sonic在go语言中的成功经验移植到不同的语言和序列化协议中。目前,sonic的c 版本正在开发中,其定位是基于sonic的核心思想和底层操作符,实现一套通用的高性能json编解码接口。

近日,sonic发布了第一个大版本v1.0.0,这表明它可以被企业灵活地用于生产环境中,也是在积极响应社区的需求,拥抱开源生态。我们期待sonic未来在使用场景和性能上有更多突破。欢迎开发者加入,贡献pr,共同打造业界最好的json库!

相关链接

项目地址:https://github.com/bytedance/sonic

基准:https://github.com/bytedance/sonic/blob/main/bench.sh

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文链接:https://www.andon8.com/28879.html

网站地图