首页 » 网站推广 » phpjson2pb技巧_Protobuf有没有比JSON快5倍用代码来击破pb机能神话

phpjson2pb技巧_Protobuf有没有比JSON快5倍用代码来击破pb机能神话

访客 2024-10-25 0

扫一扫用手机浏览

文章目录 [+]

拿 JSON 衬托 Protobuf 的文章真的太多了,常常可以看到文章中写道:“快来用 Protobuf 吧,JSON 太慢啦”。
但是 Protobuf 真的有那么牛么?我想从 JSON 切换到 Protobuf 怎么也得快一倍吧,要不然对不起付出的切换本钱?

然而,DSL-JSON 居然声称在 Java 措辞里, JSON 可以和那些二进制的编解码格式性能不相上下 [1]!

phpjson2pb技巧_Protobuf有没有比JSON快5倍用代码来击破pb机能神话

这太让人惊异了!
虽然你可能会说,咱们能不用苹果和梨来做比较了么,两个东西根本用场完备不一样,用 Protobuf 是冲着跨措辞无歧义的 IDL 的去的,才不仅仅是由于性能!
这个我赞许,但是仍旧有那么多人盲目相信 Protobuf 一定会快很多,因此我以为还是有必要通过本文彻底闭幕一下这个关于速率的传说。

phpjson2pb技巧_Protobuf有没有比JSON快5倍用代码来击破pb机能神话
(图片来自网络侵删)

DSL-JSON 的博客里只给了他们的测试结论,但是没有给出任何缘故原由,以及优化的细节。
这很难让人信服数据是真实的。
你要说 JSON 比二进制格式更快,真的是很反直觉的事情。
轻微琢磨一下这个问题,就可以列出好几个 Protobuf 该当更快的情由:

更容随意马虎绑定值到工具的字段上。
JSON 的字段是用字符串指定的,比较之下字符串比对该当比基于数字的字段 tag 更耗时。

JSON 是文本的格式,整数和浮点数该当更占空间而且更费时。

Protobuf 在正文前有一个大小或者长度的标记,而 JSON 必须全文扫描无法跳过不须要的字段。

但是仅凭这几点是不是就可以盖棺定论了呢?未必,也有相反的不雅观点:

如果字段大部分是字符串,占到决定性成分的成分可能是字符串拷贝的速率,而不是解析的速率。
在这个评测 [2] 中,我们看到不少库的性能是非常靠近的。
这是由于测试数据中大部分是由字符串构成的。

影响解析速率的决定性成分是分支的数量。
由于分支的存在,解析仍旧是一个实质上串行的过程。
虽然 Protobuf 里没有 或者 {},但是仍旧有类似的分支代码的存在。
如果没有这些分支的存在,解析不过便是一个 memcpy 的操作而已。
只有 Parabix 这样的技能才有革命性的意义,而 Protobuf 比较 JSON 只是改良而非革命。

大概 Protobuf 是一个理论上更快的格式,但是实现它的库并不一定就更快。
这取决于优化做得好不好,如果有不必要的内存分配或者重复读取,实际的速率未必就快。

有多个 benchmark 都把 DSL-JSON 列到前三名里,有时乃至比其他的二进制编码更快。
经由我仔细剖析,缘故原由出在了这些 benchmark 对付测试数据的构成选择上。
由于布局测试数据很麻烦,以是一样平常评测只会对相同的测试数据,去测不同的库的实现。
这样就使得结果是严重方向于某种类型输入的。
比如 [3] 选择的测试数据的构造是这样的

无论怎么去布局 small/medium/large 的输入,benchmark 仍旧是存在特定方向性的。
而且这种方向性是不明确的。
比如 medium 的输入,到底解释了什么?medium 对付不同的人来说,可能意味着完备不同的东西。
以是,在这里我想改变一下贱戏的规则。
不去选择一个所谓的最现实的配比,而是布局一些极度的情形。
这样,我们可以一览无余的知道,JSON 的强项和弱点都是什么。
通过把这些毛病放大出来,我们也就可以对最坏的情形有一个清晰的预期。
详细在你的场景下性能差距是若何的一个区间内,也可以大概预估出来。

好了,废话不多说了。
JMH 撸起来。
benchmark 的工具有以下几个:

Jackson:https://github.com/FasterXML/jackson-databindJava 程序里用的最多的 JSON 解析器。
benchmark 中开启了 AfterBurner 的加速特性。

DSL-JSON:https://github.com/ngs-doo/dsl-json天下上最快的 Java JSON 实现

Jsoniter: http://jsoniter.com/index.cn.html我抄袭 DSL-JSON 写的实现。
特殊申明:我是 Jsoniter 的作者。
这里提到的所有关于Jsoniter 的评测数据都不应该被盲目相信。
大部分的性能优化技巧是从 DSL-JSON 中直接抄来的。

Fastjson:https://github.com/alibaba/fastjson在中国很盛行的 JSON 解析器

Protobuf:https://github.com/google/protobuf在 RPC (远程方法调用)里非常盛行的二进制编解码格式

Thrift:https://thrift.apache.org其余一个很盛行的 RPC 编解码格式。
这里 benchmark 的是 TCompactProtocol

先从一个大略的场景入手。
毫无疑问,Protobuf 非常善于于处理整数

库比较 Jacksonns/opProtobuf8.2022124.431Thrift6.627232.761Jsoniter6.4528131.009DSL-JSON4.4840472.032Fastjson2.186555.965Jackson1181357.349

从结果上看,彷佛上风非常明显。
但是由于只有 1 个整数字段,以是可能整数解析的本钱没有占到大头。
以是,我们把测试调度工具调度为 10 个整数字段。
再比比看

库比较 Jacksonns/opProtobuf8.5171067.990Thrift2.98202921.616Jsoniter3.22187654.012DSL-JSON1.43422839.151Fastjson1.4432494.654Jackson1604894.752

这下上风就非常明显了。
毫无疑问,Protobuf 解析整数的速率是非常快的,能够达到 Jackson 的 8 倍。

DSL-JSON 比 Jackson 快很多,它的优化代码在这里

整数是直接从输入的字节里打算出来的,公式是 value = (value << 3) + (value << 1) + ind; 比较读出字符串,然后调用 Integer.valueOf ,这个实现只遍历了一遍输入,同时也避免了内存分配。

Jsoniter在这个根本上做了循环展开

编码方面情形如何呢?和编码一样的测试数据,测试结果如下:

库比较 Jacksonns/opProtobuf2.9121027.285Thrift0.172128221.323Jsoniter2.02173912.732DSL-JSON2.18161038.645Fastjson0.81431348.853Jackson1351430.048

不知道为啥,Thrift 的序列化特殊慢。
而且别的 benchmark 里 Thrift 的序列化都是算慢的。
我预测该当是实现里有不足优化的地方吧,格式该当没问题。
整数编码方面,Protobuf 是 Jackson 的 3 倍。
但是和 DSL-JSON 比起来,彷佛没有快很多。

这是由于 DSL-JSON 利用了自己的优化办法,和 JDK 的官方实现不一样

这段代码的意思是比较令人费解的。
不知道哪里就做了数字到字符串的转换了。
过程是这样的,假设输入了19823,会被分解为 19 和 823 两部分。
然后有一个 `DIGITS` 的查找表,根据这个表把 19 翻译为 \公众19\"大众,把 823 翻译为 \"大众823\公众。
个中 \公众823\"大众 并不是三个byte分开来存的,而是把 bit 放到了一个integer里,然后在 writeBuf 的时候通过位移把对应的三个byte解开的

这个实现比 JDK 自带的 Integer.toString 更快。
由于查找表预先打算好了,节省了运行时的打算本钱。

解析 JSON 的 Double 就更慢了。

库比较 Jacksonns/opProtobuf13.7592447.958Thrift7.30174052.307Jsoniter4.2302453.323Jsoniter (base64)3.25390812.895DSL-JSON2.53502287.602Fastjson1.21055454.855Jackson11271311.735

Protobuf 解析 double 是 Jackson 的 13 倍。
毫无疑问,JSON 真的不适宜存浮点数。

浮点数被去掉了点,存成了 long 类型,然后再除以对应的 10 的倍数。
如果输入是 3.1415,则会变成 31415/10000。

把 double 编码为文本格式就更困难了。

库比较 Jacksonns/opProtobuf12.71143346.157Thrift0.872093533.015Jsoniter (6 digits)6.5280252.226Jsoniter (base64)6.68272843.205DSL-JSON1.231483965.621Fastjson1.061722392.219Jackson11822478.053

解码 double 的时候,Protobuf 是 Jackson 的 13 倍。
如果你乐意捐躯精度的话,Jsoniter可以选择只保留 6 位小数。
在这个取舍下,可以好一些,但是 Protobuf 仍旧是Jsoniter的两倍。

保留 6 位小数的代码是这样写的。
把 double 的处理变成了长整数的处理。

到目前来看,我们可以说 JSON 不是为数字设计的。
如果你利用的是 Jackson,切换到 Protobuf 的话可以把数字的处理速率提高 10 倍。
然而 DSL-Json 做的优化可以把这个性能差距大幅缩小,解码在 3x ~ 4x 之间,编码在 1.3x ~ 2x 之间(条件是捐躯 double 的编码精度)。

由于 JSON 处理 double 非常慢。
以是 Jsoniter供应了一种把 double 的 IEEE 754 的二进制表示(64个bit)用 base64 编码之后保存的方案。
如果希望提高速率,但是又要保持精度,可以利用 Base64FloatSupport.enableEncodersAndDecoders;

对付 0.123456789 就变成了 \"大众OWNfmt03P78\公众

我们已经看到了 JSON 在处理数字方面的笨拙丑态了。
在处理工具绑定方面,是不是也一样不堪?前面的 benchmark 结果那么差和按字段做绑定是不是有关系?毕竟我们有 10 个字段要处理那。
这就来看看在处理字段方面的效率问题。

为了让比较起来公正一些,我们利用很短的 ascii 编码的字符串作为字段的值。
这样字符串拷贝的本钱大家都差不到哪里去。
以是性能上要有差距,一定是和按字段绑定值有关系。

库比较 Jacksonns/opProtobuf2.5268666.658Thrift2.7463139.324Jsoniter5.7829887.361DSL-JSON5.3232458.030Fastjson1.71101107.721Jackson1172747.146

如果只有一个字段,Protobuf 是 Jackson 的 2.5 倍。
但是比 DSL-JSON 要慢。

我们再把同样的实验重复几次,分别对应 5 个字段,10个字段的情形。

库比较 Jacksonns/opProtobuf1.3276972.857Thrift1.44250016.572Jsoniter2.5143807.401DSL-JSON2.41149261.728Fastjson1.39259296.397Jackson1359868.351

在有 5 个字段的情形下,Protobuf 仅仅是 Jackson 的 1.3x 倍。
如果你认为 JSON 工具绑定很慢,而且会决定 JSON 解析的整体性能。
对不起,你错了。

库比较 Jacksonns/opProtobuf1.22462167.920Thrift1.12503725.605Jsoniter2.04277531.128DSL-JSON1.84307569.103Fastjson1.18477492.445Jackson1564942.726

把字段数量加到了 10 个之后,Protobuf 仅仅是 Jackson 的 1.22 倍了。
看到这里,你该当懂了吧。

Protobuf 在处理字段绑定的时候,用的是 switch case:

这个实现比 Hashmap 来说,仅仅是轻微略快而已。
DSL-JSON 的实现是先 hash,然后也是类似的分发的办法:

利用的 hash 算法是 FNV-1a。

是 hash 就会碰撞,以是用起来须要小心。
如果输入很有可能包含未知的字段,则须要放弃速率选择匹配之后再查一下字段是不是严格相等的。
Jsoniter有一个解码模式

DYNAMIC_MODE_AND_MATCH_FIELD_STRICTLY,它可以产生下面这样的严格匹配的代码:

即便是严格匹配,速率上也是有担保的。
DSL-JSON 也有选项,可以在 hash 匹配之后额外加一次字符串 equals 检讨。

库比较 Jacksonns/opJsoniter (hash mode)2.13274949.346Jsoniter (strict mode)1.95300524.989DSL-JSON (hash mode)1.91305812.208DSL-JSON (strict mode)1.71343203.344Jackson1585421.314

关于工具绑定来说,只要字段名不长,基于数字的 tag 分发并不会比 JSON 具有明显上风,即便是比较最慢的 Jackson 来说也是如此。

废话不多说了,直接比较一下三种字段数量情形下,编码的速率

只有 1 个字段

库比较 Jacksonns/opProtobuf1.2257502.775Thrift0.86137094.627Jsoniter2.0657081.756DSL-JSON2.4647890.664Fastjson0.92127421.715Jackson1117604.479

有 5 个字段

库比较 Jacksonns/opProtobuf1.68127933.179Thrift0.46467818.566Jsoniter2.5484702.001DSL-JSON2.6880211.517Fastjson0.98219373.346Jackson1214802.686

有 10 个字段

库比较 Jacksonns/opProtobuf1.72194371.476Thrift0.38888230.783Jsoniter2.59129305.086DSL-JSON2.56130379.967Fastjson1.06315267.365Jackson1334297.953

工具编码方面,Protobuf 是 Jackson 的 1.7 倍。
但是速率实在比 DSL-Json 还要慢。

优化工具编码的办法是,一次性尽可能多的把掌握类的字节写出去。

可以看到我们把 \公众field1\"大众: 作为一个整体写出去了。
如果我们知道字段是非空的,则可以进一步的把字符串的双引号也一起合并写出去。

从工具的编解码的 benchmark 结果可以看出,Protobuf 在这个方面仅仅比 Jackson 略微强一些,而比 DSL-Json 要慢。

Protobuf 对付整数列表有特殊的支持,可以打包存储

设置 [packed=true]

库比较 Jacksonns/opProtobuf2.92249888.105Thrift3.63201439.691Jsoniter2.97245837.298DSL-JSON1.97370897.998Fastjson0.89820099.921Jackson1730450.607

对付整数列表的解码,Protobuf 是 Jackson 的 3 倍。
然而比 DSL-Json 的上风并不明显。

在 Jsoniter里,解码的循环被展开了:

对付成员比较少的情形,这样搞可以避免数组的扩容带来的内存拷贝。

Protobuf 在编码数组的时候该当有上风,不用写那么多逗号出来嘛。

库比较 Jacksonns/opProtobuf1.35159337.360Thrift0.45472555.572Jsoniter1.9112770.811DSL-JSON2.1997998.250Fastjson0.66323194.122Jackson1214409.223

Protobuf 在编码整数列表的时候,仅仅是 Jackson 的 1.35 倍。
虽然 Protobuf 在处理工具的整数字段的时候上风明显,但是在处理整数的列表时却不是如此。
在这个方面,DSL-Json 没有分外的优化,性能的提高纯粹只是由于单个数字的编码速率提高了。

列表常常用做工具的容器。
测试这种两种容器组合嵌套的场景,也很有代表意义。

库比较 Jacksonns/opProtobuf1.261118704.310Thrift1.31078278.555Jsoniter2.91483304.365DSL-JSON2.22635179.183Fastjson1.121260390.104Jackson11407116.476

Protobuf 处理工具列表是 Jackson 的 1.3 倍。
但是不及 DSL-JSON。

库比较 Jacksonns/opProtobuf2.22328219.768Thrift0.381885052.964Jsoniter3.63200420.923DSL-JSON3.87187964.594Fastjson0.85857771.520Jackson1727582.950

Protobuf 处理工具列表的编码速率是 Jackson 的 2 倍。
但是 DSL-JSON 仍旧比 Protobuf 更快。
彷佛 Protobuf 在处理列表的编码解码方面上风不明显。

Java 的数组有点分外,double 是比 List<Double> 更高效的。
利用 double 数组来代表韶光点上的值或者坐标是非常常见的做法。
然而,Protobuf 的 Java 库没有供应 double 的支持,repeated 总是利用 List<Double>。
我们可以预期 JSON 库在这里有一定的上风。

库比较 Jacksonns/opProtobuf5.18207503.316Thrift6.12175678.703Jsoniter4.83222818.772Jsoniter (base64)3.63296262.142DSL-JSON2.8383549.289Fastjson0.581866460.535Jackson11075423.265

Protobuf 在处理 double 数组方面,Jackson 与之的差距被缩小为 5 倍。
Protobuf 与 DSL-JSON 比较,上风已经不明显了。
以是如果你有很多的 double 数值须要处理,这些数值必须是在工具的字段上,才会引起性能的巨大差别,对付数组里的 double,上风差距被缩小。

在 Jsoniter里,处理数组的循环也是被展开的。

这避免了数组扩容的开销。

再来看看 double 数组的编码

库比较 Jacksonns/opProtobuf15.63107760.788Thrift0.543125678.472Jsoniter (6 digits)6.74249945.866Jsoniter (base64)7.11236991.658DSL-JSON1.141478332.248Fastjson1.081562377.465Jackson11684935.837

Protobuf 可以飞快地对 double 数组进行编码,是 Jackson 的 15 倍。
在捐躯精度的情形下,Protobuf 只是Jsoniter的 2.3 倍。
以是,再次证明了,JSON 处理 double 非常慢。
如果用 base64 编码 double,则可以保持精度,速率和捐躯精度时一样。

JSON 字符串包含了转义字符的支持。
Protobuf 解码字符串仅仅是一个内存拷贝。
理应更快才对。
被测试的字符串长度是 160 个字节的 ascii。

库比较 Jacksonns/opProtobuf1.85173680.548Thrift2.29140635.170Jsoniter2.4134067.924DSL-JSON2.27141419.108Fastjson1.14281061.212Jackson1321406.155

Protobuf 解码长字符串是 Jackson 的 1.85 倍。
然而,DSL-Json 比 Protobuf 更快。
这就有点奇怪了,JSON 的处理包袱更重,为什么会更快呢?

这个捷径里规避了处理转义字符和utf8字符串的本钱。

在 JDK9 之前,java.lang.String 都是基于 `char` 的。
而输入都是 byte 并且是 utf-8 编码的。
以是这使得,我们不能直接用 memcpy 的办法来处理字符串的解码问题。

但是在 JDK9 里,java.lang.String 已经改成了基于`byte`的了。
从 JDK9 的源代码里可以看出:

利用这个虽然被废弃,但是还没有被删除的布局函数,我们可以利用 Arrays.copyOfRange 来直接布局 java.lang.String 了。
然而,在测试之后,创造这个实现办法并没有比 DSL-JSON 的实现更快。

彷佛 JVM 的 Hotspot 动态编译时对这段循环的代码做了模式匹配,识别出了更高效的实现办法。
即便是在 JDK9 利用 +UseCompactStrings 的条件下,理论上来说本该当更慢的 byte => char => byte 并没有使得这段代码变慢,DSL-JSON 的实现还是最快的。

如果输入大部分是字符串,这个优化就变得至关主要了。
Java 里的解析艺术,还不如说是字节拷贝的艺术。
JVM 的 java.lang.String 设计实在是太屈曲了。
在当代一点的措辞中,比如 Go,字符串都是基于 utf-8 byte 的。

类似的问题,由于须要把 char 转换为 byte,以是没法直接内存拷贝。

库比较 Jacksonns/opProtobuf0.96262077.921Thrift0.99252140.935Jsoniter1.5166381.978DSL-JSON1.38181008.120Fastjson0.74339919.707Jackson1250431.354

Protobuf 在编码长字符串时,比 Jackson 略微快一点点。
统统都归咎于 char。

JSON 是一个没有 header 的格式。
由于没有 header,JSON 须要扫描每个字节才可以定位到所需的字段上。
中间可能要扫过很多不须要处理的字段。

用 PbTestWriteObject 来编码,然后用 PbTestReadObject 来解码。
field1 和 field2 的内容该当被跳过。

库比较 Jacksonns/opProtobuf5.05152194.483Thrift5.43141467.209Jsoniter3.75204704.100DSL-JSON2.51305784.845Fastjson0.41949277.734Jackson1768840.597

Protobuf 在跳过数据构造方面,是 Jackson 的 5 倍。
但是如果跳过长的字符串,JSON 的本钱是和字符串长度线性干系的,而 Protobuf 则是一个常量操作。

末了,我们把所有的战果汇总到一起。

场景Protobuf V.S. JacksonProtobuf V.S. JsoniterJsoniter V.S JacksonDecode Integer8.512.643.22Encode Integer2.91.442.02Decode Double13.753.274.2Encode Double12.711.96 (只保留小数点后6位)6.5Decode Object1.220.62.04Encode Object1.720.672.59Decode Integer List2.920.982.97Encode Integer List1.350.711.9Decode Object List1.260.432.91Encode Object List2.220.613.63Decode Double Array5.181.474.83Encode Double Array15.632.32 (只保留小数点后6位)6.74Decode String1.850.772.4Encode String0.960.631.5Skip Structure5.051.353.75

编解码数字的时候,JSON 仍旧是非常慢的。
Jsoniter把这个差距从 10 倍缩小到了 3 倍多一些。

JSON 最差的情形是下面几种:

跳过非常长的字符串:和字符串长度线性干系。

解码 double 字段:Protobuf 上风明显,是 Jsoniter 的 3.27 倍,是 Jackson 的 13.75 倍。

编码 double 字段:如果不能接管只保留 6 位小数,Protobuf 是 Jackson 的 12.71 倍。
如果接管精度丢失,Protobuf 是 Jsoniter 的 1.96 倍。

解码整数:Protobuf 是 Jsoniter 的 2.64 倍,是 Jackson 的 8.51 倍。

如果你的生产环境中的 JSON 没有那么多的 double 字段,都是字符串占大头,那么基本上来说更换成 Protobuf 也便是仅仅比 Jsoniter提高一点点,肯定在 2 倍之内。
如果不幸的话,没准 Protobuf 还要更慢一点。

标签:

相关文章

招商蛇口中国房地产龙头企业,未来可期

招商蛇口(股票代码:001979),作为中国房地产企业的领军企业,自成立以来始终秉持“以人为本,追求卓越”的经营理念,致力于打造高...

网站推广 2025-02-18 阅读0 评论0