最新要闻
- 洞庭连天是什么意思?洞庭连天九疑高是什么生肖?
- 国庆一词最早出现在什么时候?国庆一词最早出现在什么地方?
- 蝴蝶发现花蜜靠的是什么?蝴蝶发现花蜜的句子有哪些?
- 造梦西游3金角大王怎么打?造梦西游3金角大王掉什么?
- 传闻中的陈芊芊韩烁失忆是第几集?传闻中的陈芊芊韩烁失忆是真的吗?
- 爱情公寓决战紫禁之巅是第几集?爱情公寓决战紫禁之巅花了多少经费?
- 曾小莲是什么梗?爱情公寓曾小莲是哪一集?
- 睢宁县属于哪个市?睢宁县旅游景点有哪些?
- 类似于惊天魔盗团的电影有哪些?惊天魔盗团剧情解析
- 独家首发是什么意思?独家首发和普通授权有什么区别?
- 美国一战机垂直降落失控 飞行员弹射7秒落地:现场机头先撞地
- 世界时讯:可拆卸手柄神似Switch!OnexPlayer 2掌机海外发布:起售价超6200元
- 全球要闻:混动车鼻祖上新 全新丰田普锐斯售价公布:约19.10万元起
- 一加10T漫威限定版上架:用4年仍然很流畅 4700元
- 天天观焦点:三连降稳了!新一轮油价三天后开调:预计下跌0.41元/升
- 世界资讯:因MacBook Pro蝶式键盘翻车:苹果赔了3个多亿
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
IM通讯协议专题学习(五):Protobuf到底比JSON快几倍?全方位实测!
本文由陶文分享,InfoQ编辑发布,有修订和改动。
1、前言
本系列的前几篇主要是从各个角度讲解Protobuf的基本概念、技术原理这些内容,但回过头来看,对比JSON这种事实上的数据协议工业标准,Protobuf到底性能到底高多少?
【资料图】
本篇将以Protobuf为基准,对比市面上的一些主流的JSON解析库,通过全方位测试来证明给你看看Protobuf到底比JSON快几倍。
学习交流:
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步发布于:http://www.52im.net/thread-4095-1-1.html)
2、系列文章
本文是系列文章中的第5篇,本系列总目录如下:
- 《IM通讯协议专题学习(一):Protobuf从入门到精通,一篇就够!》
- 《IM通讯协议专题学习(二):快速理解Protobuf的背景、原理、使用、优缺点》
- 《IM通讯协议专题学习(三):由浅入深,从根上理解Protobuf的编解码原理》
- 《IM通讯协议专题学习(四):从Base64到Protobuf,详解Protobuf的数据编码原理》
- 《IM通讯协议专题学习(五):Protobuf到底比JSON快几倍?全方位实测!》(* 本文)
- 《IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf》(稍后发布..)
- 《IM通讯协议专题学习(七):手把手教你如何在NodeJS中从零使用Protobuf》(稍后发布..)
- 《IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇) 》(稍后发布..)
- 《IM通讯协议专题学习(九):金蝶随手记团队的Protobuf应用实践(实战篇) 》(稍后发布..)
3、写在前面
拿 JSON 衬托 Protobuf 的文章真的太多了,经常可以看到文章中写道:“快来用 Protobuf 吧,JSON 太慢啦”。
但是 Protobuf 真的有吹的那么牛么?
我觉得从 JSON 切换到 Protobuf 怎么也得快一倍吧,要不然对不起付出的切换成本。然而,DSL-JSON的家伙们居然说在Java语言里 JSON 和那些二进制的编解码格式有得一拼,这太让人惊讶了!
虽然你可能会说,咱们能不用苹果和梨来做比较了么?两个东西根本用途完全不一样好么。咱们用 Protobuf 是冲着跨语言无歧义的 IDL 的去的,才不仅仅是因为性能呢。好吧,这个我同意。但是仍然有那么多人盲目相信,Protobuf 一定会快很多,我觉得还是有必要彻底终结一下这个关于速度的传说。
DSL-JSON的博客里只给了他们的测试结论,但是没有给出任何原因,以及优化的细节,这很难让人信服数据是真实的。你要说 JSON 比二进制格式更快,真的是很反直觉的事情。
稍微琢磨一下这个问题,就可以列出好几个 Protobuf 应该更快的理由。
比如:
- 1)更容容易绑定值到对象的字段上。JSON 的字段是用字符串指定的,相比之下字符串比对应该比基于数字的字段tag更耗时;
- 2)JSON 是文本的格式,整数和浮点数应该更占空间而且更费时;
- 3)Protobuf 在正文前有一个大小或者长度的标记,而 JSON 必须全文扫描无法跳过不需要的字段。
但是仅凭这几点是不是就可以盖棺定论了呢?未必。
也有相反的观点:
- 1)如果字段大部分是字符串,占到决定性因素的因素可能是字符串拷贝的速度,而不是解析的速度。在这个评测中,我们看到不少库的性能是非常接近的。这是因为测试数据中大部分是由字符串构成的;
- 2)影响解析速度的决定性因素是分支的数量。因为分支的存在,解析仍然是一个本质上串行的过程。虽然Protobuf里没有[] 或者 {},但是仍然有类似的分支代码的存在。如果没有这些分支的存在,解析不过就是一个 memcpy 的操作而已。只有 Parabix 这样的技术才有革命性的意义,而 Protobuf 相比 JSON 只是改良而非革命;
- 3)也许 Protobuf 是一个理论上更快的格式,但是实现它的库并不一定就更快。这取决于优化做得好不好,如果有不必要的内存分配或者重复读取,实际的速度未必就快。
有多个 benchmark 都把 DSL-JSON列到前三名里,有时甚至比其他的二进制编码更快。
经过我仔细分析,原因出在了这些 benchmark 对于测试数据的构成选择上。
因为构造测试数据很麻烦,所以一般评测只会对相同的测试数据,去测不同的库的实现。
这样就使得结果是严重倾向于某种类型输入的。
比如https://github.com/eishay/jvm-serializers/wiki选择的测试数据的结构是这样的:
message Image {
required string uri = 1; //url to the thumbnail
optional string title = 2; //used in the html ALT
required int32 width = 3; // of the image
required int32 height = 4; // of the image
enum Size {
SMALL = 0;
LARGE = 1;
}
required Size size= 5; // of the image (in relative terms, provided by cnbc for example)
}
message Media {
required string uri = 1; //uri to the video, may not be an actual URL
optional string title = 2; //used in the html ALT
required int32 width = 3; // of the video
required int32 height = 4; // of the video
required string format = 5; //avi, jpg, youtube, cnbc, audio/mpeg formats ...
required int64 duration = 6; //time in miliseconds
required int64 size= 7; //file size
optional int32 bitrate = 8; //video
repeated string person = 9; //name of a person featured in the video
enum Player {
JAVA = 0;
FLASH = 1;
}
required Player player = 10; //in case of a player specific media
optional string copyright = 11;//media copyright
}
message MediaContent {
repeated Image image = 1;
required Media media = 2;
}
无论怎么去构造small/medium/large的输入,benchmark 仍然是存在特定倾向性的。
而且这种倾向性是不明确的。比如 medium 的输入,到底说明了什么?medium 对于不同的人来说,可能意味着完全不同的东西。
所以,在这里我想改变一下游戏的规则。不去选择一个所谓的最现实的配比,而是构造一些极端的情况。
这样,我们可以一目了然的知道,JSON的强项和弱点都是什么。通过把这些缺陷放大出来,我们也就可以对最坏的情况有一个清晰的预期。具体在你的场景下性能差距是怎样的一个区间内,也可以大概预估出来。
4、本次评测对象
好了,废话不多说了,JMH 撸起来。
benchmark 的对象有以下几个:
- 1)Jackson:Java 程序里用的最多的 JSON 解析器。benchmark 中开启了 AfterBurner 的加速特性;
- 2)DSL-JSON:世界上最快的 Java JSON 实现;
- 3)Jsoniter:抄袭 DSL-JSON 写的实现;
- 4)Fastjson:在中国很流行的 JSON 解析器;
- 5)Protobuf:在 RPC (远程方法调用)里非常流行的二进制编解码格式;
- 6)Thrift:另外一个很流行的 RPC 编解码格式。这里 benchmark 的是 TCompactProtocol。
5、整数解码性能测试(Decode Integer)
先从一个简单的场景入手。
毫无疑问,Protobuf 非常擅长于处理整数:
message PbTestObject {
int32 field1 = 1;
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_int
从结果上看,似乎优势非常明显。但是因为只有 1 个整数字段,所以可能整数解析的成本没有占到大头。
所以,我们把测试调整对象调整为 10 个整数字段。再比比看:
syntax = "proto3";
option optimize_for = SPEED;
message PbTestObject {
int32 field1 = 1;
int32 field2 = 2;
int32 field3 = 3;
int32 field4 = 4;
int32 field5 = 5;
int32 field6 = 6;
int32 field7 = 7;
int32 field8 = 8;
int32 field9 = 9;
int32 field10 = 10;
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_10_int_fields
这下优势就非常明显了。毫无疑问,Protobuf 解析整数的速度是非常快的,能够达到 Jackson 的 8 倍。
DSL-JSON 比 Jackson 快很多,它的优化代码在这里:
private static int parsePositiveInt(final byte[] buf, final JsonReader reader, final int start, final int end, int i) throwsIOException {
int value = 0;
for(; i < end; i++) {
final int ind = buf[i ] - 48;
if(ind < 0|| ind > 9) {
... // abbreviated
}
value = (value << 3) + (value << 1) + ind;
if(value < 0) {
throw new IOException("Integer overflow detected at position: "+ reader.positionInStream(end - start));
}
}
return value;
}
整数是直接从输入的字节里计算出来的,公式是value = (value << 3) + (value << 1) + ind;相比读出字符串,然后调用 Integer.valueOf ,这个实现只遍历了一遍输入,同时也避免了内存分配。
Jsoniter 在这个基础上做了循环展开:
... // abbreviated
int i = iter.head;
int ind2 = intDigits[iter.buf[i ]];
if(ind2 == INVALID_CHAR_FOR_NUMBER) {
iter.head = i;
return ind;
}
int ind3 = intDigits[iter.buf[++i]];
if(ind3 == INVALID_CHAR_FOR_NUMBER) {
iter.head = i;
return ind * 10+ ind2;
}
int ind4 = intDigits[iter.buf[++i]];
if(ind4 == INVALID_CHAR_FOR_NUMBER) {
iter.head = i;
return ind * 100+ ind2 * 10+ ind3;
}
... // abbreviated
6、整数编码性能测试(Encode Integer)
编码方面情况如何呢?和编码一样的测试数据,测试结果如下:
不知道为啥,Thrift 的序列化特别慢。而且别的 benchmark 里 Thrift 的序列化都是算慢的。我猜测应该是实现里有不够优化的地方吧,格式应该没问题。整数编码方面,Protobuf 是 Jackson 的 3 倍。但是和 DSL-JSON 比起来,好像没有快很多。
这是因为 DSL-JSON 使用了自己的优化方式,和 JDK 的官方实现不一样(代码点此查看):
private static int serialize(final byte[] buf, int pos, final int value) {
int i;
if(value < 0) {
if(value == Integer.MIN_VALUE) {
for(intx = 0; x < MIN_INT.length; x++) {
buf[pos + x] = MIN_INT[x];
}
return pos + MIN_INT.length;
}
i = -value;
buf[pos++] = MINUS;
} else{
i = value;
}
final int q1 = i / 1000;
if(q1 == 0) {
pos += writeFirstBuf(buf, DIGITS[i ], pos);
return pos;
}
final int r1 = i - q1 * 1000;
final int q2 = q1 / 1000;
if(q2 == 0) {
final int v1 = DIGITS[r1];
final int v2 = DIGITS[q1];
int off = writeFirstBuf(buf, v2, pos);
writeBuf(buf, v1, pos + off);
return pos + 3+ off;
}
final int r2 = q1 - q2 * 1000;
final long q3 = q2 / 1000;
final int v1 = DIGITS[r1];
final int v2 = DIGITS[r2];
if(q3 == 0) {
pos += writeFirstBuf(buf, DIGITS[q2], pos);
} else{
final int r3 = (int) (q2 - q3 * 1000);
buf[pos++] = (byte) (q3 + "0");
writeBuf(buf, DIGITS[r3], pos);
pos += 3;
}
writeBuf(buf, v2, pos);
writeBuf(buf, v1, pos + 3);
return pos + 6;
}
这段代码的意思是比较令人费解的。不知道哪里就做了数字到字符串的转换了。
过程是这样的,假设输入了19823,会被分解为 19 和 823 两部分。然后有一个 `DIGITS` 的查找表,根据这个表把 19 翻译为 "19",把 823 翻译为 "823"。其中 "823" 并不是三个byte分开来存的,而是把bit放到了一个integer里,然后在 writeBuf 的时候通过位移把对应的三个byte解开的。
private static void writeBuf(final byte[] buf, final int v, int pos) {
buf[pos] = (byte) (v >> 16);
buf[pos + 1] = (byte) (v >> 8);
buf[pos + 2] = (byte) v;
}
这个实现比 JDK 自带的 Integer.toString 更快。因为查找表预先计算好了,节省了运行时的计算成本。
7、双精度浮点数解码性能测试(Decode Double)
解析 JSON 的 Double 就更慢了。
message PbTestObject {
double field1 = 1;
double field2 = 2;
double field3 = 3;
double field4 = 4;
double field5 = 5;
double field6 = 6;
double field7 = 7;
double field8 = 8;
double field9 = 9;
double field10 = 10;
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_10_double_fields
Protobuf 解析 double 是 Jackson 的 13 倍。毫无疑问,JSON真的不适合存浮点数。
DSL-Json 中对 Double 也是做了特别优化的(详见源码):
private static double parsePositiveDouble(final byte[] buf, final JsonReader reader, final int start, final int end, int i) throws IOException {
long value = 0;
byte ch = " ";
for(; i < end; i++) {
ch = buf[i ];
if(ch == ".") break;
final int ind = buf[i ] - 48;
value = (value << 3) + (value << 1) + ind;
if(ind < 0|| ind > 9) {
return parseDoubleGeneric(reader.prepareBuffer(start), end - start, reader);
}
}
if(i == end) return value;
else if(ch == ".") {
i++;
long div = 1;
for(; i < end; i++) {
final int ind = buf[i ] - 48;
div = (div << 3) + (div << 1);
value = (value << 3) + (value << 1) + ind;
if(ind < 0|| ind > 9) {
return parseDoubleGeneric(reader.prepareBuffer(start), end - start, reader);
}
}
return value / (double) div;
}
return value;
}
浮点数被去掉了点,存成了 long 类型,然后再除以对应的10的倍数。如果输入是3.1415,则会变成 31415/10000。
8、双精度浮点数编码性能测试(Encode Double)
把 double 编码为文本格式就更困难了。
解码 double 的时候,Protobuf 是 Jackson 的13 倍。如果你愿意牺牲精度的话,Jsoniter可以选择只保留6位小数。在这个取舍下,可以好一些,但是 Protobuf 仍然是Jsoniter的两倍。
保留6位小数的代码是这样写的,把 double 的处理变成了长整数的处理:
if(val < 0) {
val = -val;
stream.write("-");
}
if(val > 0x4ffffff) {
stream.writeRaw(Double.toString(val));
return;
}
int precision = 6;
int exp = 1000000; // 6
long lval = (long)(val * exp + 0.5);
stream.writeVal(lval / exp);
long fval = lval % exp;
if(fval == 0) {
return;
}
stream.write(".");
if(stream.buf.length - stream.count < 10) {
stream.flushBuffer();
}
for(int p = precision - 1; p > 0&& fval < POW10[p]; p--) {
stream.buf[stream.count++] = "0";
}
stream.writeVal(fval);
while(stream.buf[stream.count-1] == "0") {
stream.count--;
}
到目前来看,我们可以说 JSON 不是为数字设计的。如果你使用的是 Jackson,切换到 Protobuf 的话可以把数字的处理速度提高 10 倍。然而 DSL-Json 做的优化可以把这个性能差距大幅缩小,解码在 3x ~ 4x 之间,编码在 1.3x ~ 2x 之间(前提是牺牲 double 的编码精度)。
因为 JSON 处理 double 非常慢。所以 Jsoniter 提供了一种把 double 的 IEEE 754 的二进制表示(64个bit)用 base64 编码之后保存的方案。如果希望提高速度,但是又要保持精度,可以使用Base64FloatSupport.enableEncodersAndDecoders();。
long bits = Double.doubleToRawLongBits(number.doubleValue());
Base64.encodeLongBits(bits, stream);
static void encodeLongBits(long bits, JsonStream stream) throws IOException {
int i = (int) bits;
byte b1 = BA[(i >>> 18) & 0x3f];
byte b2 = BA[(i >>> 12) & 0x3f];
byte b3 = BA[(i >>> 6) & 0x3f];
byte b4 = BA[i & 0x3f];
stream.write((byte)""", b1, b2, b3, b4);
bits = bits >>> 24;
i = (int) bits;
b1 = BA[(i >>> 18) & 0x3f];
b2 = BA[(i >>> 12) & 0x3f];
b3 = BA[(i >>> 6) & 0x3f];
b4 = BA[i & 0x3f];
stream.write(b1, b2, b3, b4);
bits = (bits >>> 24) << 2;
i = (int) bits;
b1 = BA[i >> 12];
b2 = BA[(i >>> 6) & 0x3f];
b3 = BA[i & 0x3f];
stream.write(b1, b2, b3, (byte)""");
}
对于 0.123456789 就变成了 "OWNfmt03P78".
9、对象解码性能测试(Decode Object)
我们已经看到了 JSON 在处理数字方面的笨拙丑态了。在处理对象绑定方面,是不是也一样不堪?
前面的 benchmark 结果那么差和按字段做绑定是不是有关系?毕竟我们有 10 个字段要处理那。这就来看看在处理字段方面的效率问题。
为了让比较起来公平一些,我们使用很短的 ascii 编码的字符串作为字段的值。这样字符串拷贝的成本大家都差不到哪里去。
所以性能上要有差距,必然是和按字段绑定值有关系。
message PbTestObject {
string field1 = 1;
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_1_string_field
如果只有一个字段,Protobuf 是 Jackson 的 2.5 倍。但是比 DSL-JSON 要慢。
我们再把同样的实验重复几次,分别对应 5 个字段,10个字段的情况。
message PbTestObject {
string field1 = 1;
string field2 = 2;
string field3 = 3;
string field4 = 4;
string field5 = 5;
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_5_string_fields
在有 5 个字段的情况下,Protobuf 仅仅是 Jackson 的 1.3x 倍。如果你认为 JSON 对象绑定很慢,而且会决定 JSON 解析的整体性能。对不起,你错了。
message PbTestObject {
string field1 = 1;
string field2 = 2;
string field3 = 3;
string field4 = 4;
string field5 = 5;
string field6 = 6;
string field7 = 7;
string field8 = 8;
string field9 = 9;
string field10 = 10;
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_10_string_fields
把字段数量加到了 10 个之后,Protobuf 仅仅是 Jackson 的 1.22 倍了。看到这里,你应该懂了吧。
Protobuf 在处理字段绑定的时候,用的是 switch case:
boolean done = false;
while(!done) {
int tag = input.readTag();
switch(tag) {
case 0:
done = true;
break;
default: {
if(!input.skipField(tag)) {
done = true;
}
break;
}
case 10: {
java.lang.String s = input.readStringRequireUtf8();
field1_ = s;
break;
}
case 18: {
java.lang.String s = input.readStringRequireUtf8();
field2_ = s;
break;
}
case 26: {
java.lang.String s = input.readStringRequireUtf8();
field3_ = s;
break;
}
case 34: {
java.lang.String s = input.readStringRequireUtf8();
field4_ = s;
break;
}
case 42: {
java.lang.String s = input.readStringRequireUtf8();
field5_ = s;
break;
}
}
}
这个实现比 Hashmap 来说,仅仅是稍微略快而已。
DSL-JSON 的实现是先 hash,然后也是类似的分发的方式:
switch(nameHash) {
case 1212206434:
_field1_ = com.dslplatform.json.StringConverter.deserialize(reader);
nextToken = reader.getNextToken();
break;
case 1178651196:
_field3_ = com.dslplatform.json.StringConverter.deserialize(reader);
nextToken = reader.getNextToken();
break;
case 1195428815:
_field2_ = com.dslplatform.json.StringConverter.deserialize(reader);
nextToken = reader.getNextToken();
break;
case 1145095958:
_field5_ = com.dslplatform.json.StringConverter.deserialize(reader);
nextToken = reader.getNextToken();
break;
case 1161873577:
_field4_ = com.dslplatform.json.StringConverter.deserialize(reader);
nextToken = reader.getNextToken();
break;
default:
nextToken = reader.skip();
break;
}
使用的 hash 算法是 FNV-1a:
long hash = 0x811c9dc5;
while(ci < buffer.length) {
final byte b = buffer[ci++];
if(b == """) break;
hash ^= b;
hash *= 0x1000193;
}
是 hash 就会碰撞,所以用起来需要小心。如果输入很有可能包含未知的字段,则需要放弃速度选择匹配之后再查一下字段是不是严格相等的。
Jsoniter 有一个解码模式 DYNAMIC_MODE_AND_MATCH_FIELD_STRICTLY,它可以产生下面这样的严格匹配的代码:
switch(field.len()) {
case 6:
if(field.at(0) == 102&&
field.at(1) == 105&&
field.at(2) == 101&&
field.at(3) == 108&&
field.at(4) == 100) {
if(field.at(5) == 49) {
obj.field1 = (java.lang.String) iter.readString();
continue;
}
if(field.at(5) == 50) {
obj.field2 = (java.lang.String) iter.readString();
continue;
}
if(field.at(5) == 51) {
obj.field3 = (java.lang.String) iter.readString();
continue;
}
if(field.at(5) == 52) {
obj.field4 = (java.lang.String) iter.readString();
continue;
}
if(field.at(5) == 53) {
obj.field5 = (java.lang.String) iter.readString();
continue;
}
}
break;
}
iter.skip();
即便是严格匹配,速度上也是有保证的。DSL-JSON 也有选项,可以在 hash 匹配之后额外加一次字符串 equals 检查。
关于对象绑定来说,只要字段名不长,基于数字的 tag 分发并不会比 JSON 具有明显优势,即便是相比最慢的 Jackson 来说也是如此。
10、对象编码性能测试(Encode Object)
废话不多说了,直接比较一下三种字段数量情况下,编码的速度。
只有 1 个字段:
有 5 个字段:
有 10 个字段:
对象编码方面,Protobuf 是 Jackson 的 1.7 倍。但是速度其实比 DSL-Json 还要慢。
优化对象编码的方式是,一次性尽可能多的把控制类的字节写出去。
public void encode(Object obj, com.jsoniter.output.JsonStream stream) throws java.io.IOException {
if(obj == null) { stream.writeNull(); return; }
stream.write((byte)"{");
encode_((com.jsoniter.benchmark.with_1_string_field.TestObject)obj, stream);
stream.write((byte)"}");
}
public static void encode_(com.jsoniter.benchmark.with_1_string_field.TestObject obj, com.jsoniter.output.JsonStream stream) throws java.io.IOException {
boolean notFirst = false;
if(obj.field1 != null) {
if(notFirst) { stream.write(","); } else{ notFirst = true; }
stream.writeRaw("\"field1\":", 9);
stream.writeVal((java.lang.String)obj.field1);
}
}
可以看到我们把 "field1": 作为一个整体写出去了。如果我们知道字段是非空的,则可以进一步的把字符串的双引号也一起合并写出去。
public void encode(Object obj, com.jsoniter.output.JsonStream stream) throws java.io.IOException {
if(obj == null) { stream.writeNull(); return; }
stream.writeRaw("{\"field1\":\"", 11);
encode_((com.jsoniter.benchmark.with_1_string_field.TestObject)obj, stream);
stream.write((byte)"\"", (byte)"}");
}
public static void encode_(com.jsoniter.benchmark.with_1_string_field.TestObject obj, com.jsoniter.output.JsonStream stream) throws java.io.IOException {
com.jsoniter.output.CodegenAccess.writeStringWithoutQuote((java.lang.String)obj.field1, stream);
}
从对象的编解码的 benchmark 结果可以看出,Protobuf 在这个方面仅仅比 Jackson 略微强一些,而比 DSL-Json 要慢。
11、整形列表解码性能测试(Decode Integer List)
Protobuf 对于整数列表有特别的支持,可以打包存储:
22// tag (field number 4, wire type 2)
06// payload size (6 bytes)
03// first element (varint 3)
8E 02// second element (varint 270)
9E A7 05// third element (varint 86942)
设置 [packed=true]
message PbTestObject {
repeated int32 field1 = 1[packed=true];
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_int_list
对于整数列表的解码,Protobuf 是 Jackson 的 3 倍。然而比 DSL-Json 的优势并不明显。
在 Jsoniter 里,解码的循环被展开了:
public static java.lang.Object decode_(com.jsoniter.JsonIterator iter) throws java.io.IOException {
java.util.ArrayList col = (java.util.ArrayList)com.jsoniter.CodegenAccess.resetExistingObject(iter);
if(iter.readNull()) { com.jsoniter.CodegenAccess.resetExistingObject(iter); returnnull; }
if(!com.jsoniter.CodegenAccess.readArrayStart(iter)) {
returncol == null? newjava.util.ArrayList(0): (java.util.ArrayList)com.jsoniter.CodegenAccess.reuseCollection(col);
}
Object a1 = java.lang.Integer.valueOf(iter.readInt());
if(com.jsoniter.CodegenAccess.nextToken(iter) != ",") {
java.util.ArrayList obj = col == null? newjava.util.ArrayList(1): (java.util.ArrayList)com.jsoniter.CodegenAccess.reuseCollection(col);
obj.add(a1);
return obj;
}
Object a2 = java.lang.Integer.valueOf(iter.readInt());
if(com.jsoniter.CodegenAccess.nextToken(iter) != ",") {
java.util.ArrayList obj = col == null? newjava.util.ArrayList(2): (java.util.ArrayList)com.jsoniter.CodegenAccess.reuseCollection(col);
obj.add(a1);
obj.add(a2);
return obj;
}
Object a3 = java.lang.Integer.valueOf(iter.readInt());
if(com.jsoniter.CodegenAccess.nextToken(iter) != ",") {
java.util.ArrayList obj = col == null? newjava.util.ArrayList(3): (java.util.ArrayList)com.jsoniter.CodegenAccess.reuseCollection(col);
obj.add(a1);
obj.add(a2);
obj.add(a3);
return obj;
}
Object a4 = java.lang.Integer.valueOf(iter.readInt());
java.util.ArrayList obj = col == null? newjava.util.ArrayList(8): (java.util.ArrayList)com.jsoniter.CodegenAccess.reuseCollection(col);
obj.add(a1);
obj.add(a2);
obj.add(a3);
obj.add(a4);
while(com.jsoniter.CodegenAccess.nextToken(iter) == ",") {
obj.add(java.lang.Integer.valueOf(iter.readInt()));
}
return obj;
}
对于成员比较少的情况,这样搞可以避免数组的扩容带来的内存拷贝。
12、整形列表编码性能测试(Encode Integer List)
Protobuf 在编码数组的时候应该有优势,不用写那么多逗号出来嘛。
Protobuf 在编码整数列表的时候,仅仅是 Jackson 的 1.35 倍。
虽然 Protobuf 在处理对象的整数字段的时候优势明显,但是在处理整数的列表时却不是如此。在这个方面,DSL-Json 没有特殊的优化,性能的提高纯粹只是因为单个数字的编码速度提高了。
13、对象列表解码性能测试(Decode Object List)
列表经常用做对象的容器。测试这种两种容器组合嵌套的场景,也很有代表意义。
message PbTestObject {
message ElementObject {
string field1 = 1;
}
repeated ElementObject field1 = 1;
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_object_list
Protobuf 处理对象列表是 Jackson 的 1.3 倍。但是不及 DSL-JSON。
14、对象列表编码性能测试(Encode Object List)
Protobuf 处理对象列表的编码速度是 Jackson 的 2 倍。但是 DSL-JSON 仍然比 Protobuf 更快。似乎 Protobuf 在处理列表的编码解码方面优势不明显。
15、双精度浮点数数组解码性能测试(Decode Double Array)
Java 的数组有点特殊,double[] 是比 List
然而,Protobuf 的 Java库没有提供double[] 的支持,repeated 总是使用 List
message PbTestObject {
repeated doublefield1 = 1[packed=true];
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_double_array
Protobuf 在处理 double 数组方面,Jackson 与之的差距被缩小为 5 倍。Protobuf 与 DSL-JSON 相比,优势已经不明显了。所以如果你有很多的 double 数值需要处理,这些数值必须是在对象的字段上,才会引起性能的巨大差别,对于数组里的 double,优势差距被缩小。
在 Jsoniter 里,处理数组的循环也是被展开的。
public static java.lang.Object decode_(com.jsoniter.JsonIterator iter) throws java.io.IOException {
... // abbreviated
nextToken = com.jsoniter.CodegenAccess.nextToken(iter);
if(nextToken == "]") {
return new double[0];
}
com.jsoniter.CodegenAccess.unreadByte(iter);
double a1 = iter.readDouble();
if(!com.jsoniter.CodegenAccess.nextTokenIsComma(iter)) {
return new double[]{ a1 };
}
double a2 = iter.readDouble();
if(!com.jsoniter.CodegenAccess.nextTokenIsComma(iter)) {
return new double[]{ a1, a2 };
}
double a3 = iter.readDouble();
if(!com.jsoniter.CodegenAccess.nextTokenIsComma(iter)) {
return new double[]{ a1, a2, a3 };
}
double a4 = (double) iter.readDouble();
if(!com.jsoniter.CodegenAccess.nextTokenIsComma(iter)) {
return new double[]{ a1, a2, a3, a4 };
}
double a5 = (double) iter.readDouble();
double[] arr = new double[10];
arr[0] = a1;
arr[1] = a2;
arr[2] = a3;
arr[3] = a4;
arr[4] = a5;
inti = 5;
while(com.jsoniter.CodegenAccess.nextTokenIsComma(iter)) {
if(i == arr.length) {
double[] newArr = new double[arr.length * 2];
System.arraycopy(arr, 0, newArr, 0, arr.length);
arr = newArr;
}
arr[i++] = iter.readDouble();
}
double[] result = newdouble[i ];
System.arraycopy(arr, 0, result, 0, i);
return result;
}
这避免了数组扩容的开销。
16、双精度浮点数数组编码性能测试(Encode Double Array)
再来看看 double 数组的编码:
Protobuf 可以飞快地对 double 数组进行编码,是 Jackson 的 15 倍。在牺牲精度的情况下,Protobuf 只是Jsoniter 的 2.3 倍。
所以,再次证明了,JSON 处理 double 非常慢。如果用 base64 编码 double,则可以保持精度,速度和牺牲精度时一样。
17、字符串解码性能测试(Decode String)
JSON 字符串包含了转义字符的支持。Protobuf 解码字符串仅仅是一个内存拷贝。理应更快才对。被测试的字符串长度是 160 个字节的 ascii。
syntax = "proto3";
option optimize_for = SPEED;
message PbTestObject {
string field1 = 1;
}
https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_long_string
Protobuf 解码长字符串是 Jackson 的 1.85 倍。然而,DSL-Json 比 Protobuf 更快。这就有点奇怪了,JSON 的处理负担更重,为什么会更快呢?
先尝试捷径:
DSL-JSON 给 ascii 实现了一个捷径(源码点此):
for(int i = 0; i < chars.length; i++) {
bb = buffer[ci++];
if(bb == """) {
currentIndex = ci;
return i;
}
// If we encounter a backslash, which is a beginning of an escape sequence
// or a high bit was set - indicating an UTF-8 encoded multibyte character,
// there is no chance that we can decode the string without instantiating
// a temporary buffer, so quit this loop
if((bb ^ "\\") < 1) break;
chars[i ] = (char) bb;
}
这个捷径里规避了处理转义字符和utf8字符串的成本。
JVM 的动态编译做了特殊优化:
在 JDK9 之前,java.lang.String 都是基于 `char[]` 的。而输入都是 byte[] 并且是 utf-8 编码的。所以这使得,我们不能直接用 memcpy 的方式来处理字符串的解码问题。
但是在 JDK9 里,java.lang.String 已经改成了基于`byte[]`的了。
从 JDK9 的源代码里可以看出:
@Deprecated(since="1.1")
public String(byte ascii[], int hibyte, int offset, int count) {
checkBoundsOffCount(offset, count, ascii.length);
if(count == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if(COMPACT_STRINGS && (byte)hibyte == 0) {
this.value = Arrays.copyOfRange(ascii, offset, offset + count);
this.coder = LATIN1;
} else{
hibyte <<= 8;
byte[] val = StringUTF16.newBytesFor(count);
for(inti = 0; i < count; i++) {
StringUTF16.putChar(val, i, hibyte | (ascii[offset++] & 0xff));
}
this.value = val;
this.coder = UTF16;
}
}
使用这个虽然被废弃,但是还没有被删除的构造函数,我们可以使用 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[] 的。
18、字符串编码性能测试(Encode String)
类似的问题,因为需要把 char[] 转换为 byte[],所以没法直接内存拷贝。
Protobuf 在编码长字符串时,比 Jackson 略微快一点点,一切都归咎于 char[]。
19、本文总结
最后,我们把所有的战果汇总到一起。
编解码数字的时候,JSON仍然是非常慢的。Jsoniter 把这个差距从 10 倍缩小到了 3 倍多一些。
JSON 最差的情况是下面几种:
- 1)跳过非常长的字符串:和字符串长度线性相关;
- 2)解码 double 字段:Protobuf 优势明显,是 Jsoniter的 3.27 倍,是 Jackson 的 13.75 倍;
- 3)编码 double 字段:如果不能接受只保留 6 位小数,Protobuf 是 Jackson 的 12.71 倍(如果接受精度损失,Protobuf 是 Jsoniter 的 1.96 倍);
- 4)解码整数:Protobuf 是 Jsoniter 的 2.64 倍,是 Jackson 的 8.51 倍。
如果你的生产环境中的JSON没有那么多的double字段,都是字符串占大头,那么基本上来说替换成 Protobuf 也就是仅仅比 Jsoniter 提高一点点,肯定在2倍之内。如果不幸的话,没准 Protobuf 还要更慢一点。
20、参考资料
[1]Protobuf官方编码资料
[2]Protobuf官方手册
[3]Why do we use Base64?
[4]The Base16, Base32, and Base64 Data Encodings
[5]Protobuf从入门到精通,一篇就够!
[5]如何选择即时通讯应用的数据传输格式
[7]强列建议将Protobuf作为你的即时通讯应用数据传输格式
[8]APP与后台通信数据格式的演进:从文本协议到二进制协议
[9]面试必考,史上最通俗大小端字节序详解
[10]移动端IM开发需要面对的技术问题(含通信协议选择)
[11]简述移动端IM开发的那些坑:架构设计、通信协议和客户端
[12]理论联系实际:一套典型的IM通信协议设计详解
[13]58到家实时消息系统的协议设计等技术实践分享
(本文已同步发布于:http://www.52im.net/thread-4095-1-1.html)
-
IM通讯协议专题学习(五):Protobuf到底比JSON快几倍?全方位实测!
本篇将以Protobuf为基准,对比市面上的一些主流的JSON解析库,通过全方位测试来证明给你看看Protobuf到...
来源: IM通讯协议专题学习(五):Protobuf到底比JSON快几倍?全方位实测!
【聚看点】多数据源事务处理-涉及分布式事务
怎么硬盘安装ubuntu?硬盘ubuntu安装教程
佳能5d系列哪个最好?佳能5DX相机参数
诺基亚6200上市价格是多少?诺基亚6200手机参数
烤箱如何预热?烤箱预热的方法有哪些?
短讯!IDEA没有新建jsp文件按钮
VS2022生成控制台引用程序,.net应用导出成exe文件,发部成独立文件的详细图解
MySQL学习笔记2
网页字体变大是怎么回事?网页字体变大了怎么还原?
洞庭连天是什么意思?洞庭连天九疑高是什么生肖?
国庆一词最早出现在什么时候?国庆一词最早出现在什么地方?
蝴蝶发现花蜜靠的是什么?蝴蝶发现花蜜的句子有哪些?
造梦西游3金角大王怎么打?造梦西游3金角大王掉什么?
传闻中的陈芊芊韩烁失忆是第几集?传闻中的陈芊芊韩烁失忆是真的吗?
爱情公寓决战紫禁之巅是第几集?爱情公寓决战紫禁之巅花了多少经费?
曾小莲是什么梗?爱情公寓曾小莲是哪一集?
睢宁县属于哪个市?睢宁县旅游景点有哪些?
类似于惊天魔盗团的电影有哪些?惊天魔盗团剧情解析
独家首发是什么意思?独家首发和普通授权有什么区别?
环球短讯!认证管理(锐捷网关篇)
世界即时看!C语言字符串拆分的两种方式strtok和正则表达式
全球通讯!低代码靠不靠谱?看看低代码在智能物联系统搭建中的应用
美国一战机垂直降落失控 飞行员弹射7秒落地:现场机头先撞地
世界时讯:可拆卸手柄神似Switch!OnexPlayer 2掌机海外发布:起售价超6200元
全球要闻:混动车鼻祖上新 全新丰田普锐斯售价公布:约19.10万元起
一加10T漫威限定版上架:用4年仍然很流畅 4700元
天天观焦点:三连降稳了!新一轮油价三天后开调:预计下跌0.41元/升
世界资讯:因MacBook Pro蝶式键盘翻车:苹果赔了3个多亿
速递!贾跃亭造车梦要成了?FF91交付计划公布 还差10几亿资金
全球热议:爱奇艺VIP会员今起涨价:连续包月黄金25元/月
今日热讯:浙江“火流星”现场被砸大坑!专家:捡到陨石碎片别用水清洗
焦点精选!机械硬盘真没人买!西数股价大跌 公司业绩要暴雷:或将大清库存
每日精选:V2Board机场项目泄露400余万条数据
环球快报:《阿凡达2:水之道》今日内地正式上映:票房瞬间破亿
日系车为什么在中国卖不动了?你为什么不买了?
每日速讯:Blazor和Vue对比学习(进阶.路由导航五):路由守卫
观点:【从零开始学爬虫】采集收视率排行数据
mvn 打包报错:no compiler is provided in this environment
天天微动态丨JavaScript DOM的性能优化详解
每日资讯:VUE的实例的生命周期
每日热门:河洛肉鸽卡牌《天外武林》上架Steam 明年1月发售
别急换机!本月还有6场发布会:劝你先等等再买
世界微动态丨《阿凡达:水之道》预测票房仅25.11亿!远不到《长津湖》一半
【环球新要闻】Epic大促开启 连续15天免费送游戏!75折套娃优惠券来了
【天天播资讯】RTX 4070 Ti跑分首曝:猛升46%、超越RX 7900 XTX
焦点热讯:替代if esle 的高级方法
make学习
天天热门:高手必备10大难题:Mysql如何实现RR级隔离时,不会幻读?
FreeSWITCH学习笔记:应用程序(APP)
速度飞起!全球首款232层闪存SSD细节公布:TLC颗粒、拒绝PCIe 5.0
快资讯:一图看懂moto新机:二代骁龙8 3699元!价格屠夫简直了
全球观察:7-2 案例 字符串关键字的散列映射
天天热议:微软2025年目标:确保非洲再增加1亿人访问互联网
焦点信息:火球坠入浙江 警方带走疑似陨石碎片:现场砸出六七厘米深坑
环球微头条丨初始Docker
热点评!分智慧果 - 2021算法与数据结构实验题
环球观热点:不调好不上市 moto X40驯服二代骁龙8:1小时王者温度仅42度
Nature年度十大人物出炉:猪心移植手术团队负责人等入选
饱满如水滴!联想moto X40开箱图赏
5000mAh大电池+120Hz屏 899元联想moto G53图赏
109度大电池能跑730KM!岚图追光预售:32.29万元起
天天微动态丨自研分布式高性能RPC框架及服务注册中心ApiRegistry实践笔记【原创】【开源】
【独家】图形用户界面(GUI)编程可以学习C++ Builder,多图、实例、书籍
17 Java内存模型与线程_Java与线程
视焦点讯!努比亚Z50散热堆料离谱:散热总面积达到36462m㎡
中国光纤之父赵梓森逝世 享年91岁:拉出了我国第一根光纤
世界观热点:苹果妥协了!将开门迎接第三方应用商店 用户褒贬不一
杭州疑似有陨石掉落:一团火球 照亮夜空
环球要闻:AIRIOT答疑第2期|如何使用物联网平台的数据采集与控制引擎?
《隐秘的角落》游戏来了:Steam版定档明年1月18日
【时快讯】可以薅羊毛了!网易云音乐TV版正式上线:免费送3个月会员
天天快报!男子花12万买世界杯彩票没中要求退款 理由气煞店老板
天天热头条丨微信全新朋友圈点赞互动广告上线:点下就能放烟花秀
天天热资讯!下单15分钟送到:上海开通首条美团无人机常态化航线
世界速读:BaseDet: 走过开发的弯路
今日热门!引迈信息荣登「2022低代码企业50强」:JNPF,你的不二选择!
环球关注:IMDb 8.2分!《阿凡达2》口碑爆棚 影院回应票价高:消费者不缺钱缺服务
OPPO Find N2 Flip外屏惊艳:支持快捷回复、锁屏小游戏
第二家通吃NVIDIA、AMD、Intel的显卡品牌诞生!这也太贵了
天天实时:公牛发布航天级品质超薄插座:可承受2万次拔插
环球微头条丨跌破零下40度!内蒙古出现极寒冰雾天气:冷到“冒白烟”
环球微速讯:SQL的使用总结
天天快资讯:WPF深入简出(一)入门必看
观热点:【机器学习】李宏毅——浅谈机器学习原理+鱼与熊掌兼得的深度学习简述
当前动态:前后端AES加密解密,CryptoJS和Java实现
热讯:记录--手把手带你开发一个uni-app日历插件(并发布)
OPPO Find N成为全国销量第二折叠屏!一经发布就大获成功
微速讯:首次使用钛合金螺丝!OPPO Find N2仅233g:折叠屏比iPhone还轻
OPPO Find N2内外一致好屏:双120Hz+双E6 可自由悬停
让玩家心动的定制 ROG 6游戏手机《暗黑破坏神:不朽》典藏限量版图赏
OPPO Find N2 233g比直板机还轻!央视点赞:刷新折叠屏手机新纪录
【世界速看料】LeetCode HOT 100:旋转图像
环球头条:特斯拉中国上线490元酒杯 限量卖 网友吐槽智商税
世界今日讯!女子连吃5天感冒药 多脏器功能衰竭:专家提醒小心这些症状
IT工程师哪里工资最高?美国仅排第二 第一收入超过70万
天天看热讯:帧数暴涨40%!《巫师3》次时代版画质优化指南
20万级大杀器 长安深蓝首款SUV曝光:增程、纯电全都有
【天天速看料】Shell 标准输入和输出
信息:公司新来一个同事,把网关系统设计的炉火纯青!(万能通用,稳的一批。。)