关于即时通讯:IM通讯协议专题学习五Protobuf到底比JSON快几倍全方位实测

32次阅读

共计 22281 个字符,预计需要花费 56 分钟才能阅读完成。

本文由陶文分享,InfoQ 编辑公布,有订正和改变。

1、前言

本系列的前几篇次要是从各个角度解说 Protobuf 的基本概念、技术原理这些内容,但回过头来看,比照 JSON 这种事实上的数据协定工业规范,Protobuf 到底性能到底高多少?本篇将以 Protobuf 为基准,比照市面上的一些支流的 JSON 解析库,通过全方位测试来证实给你看看 Protobuf 到底比 JSON 快几倍。

学习交换:

  • 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》
  • 开源 IM 框架源码:https://github.com/JackJiang2…(备用地址点此)
    (本文已同步公布于:http://www.52im.net/thread-40…)

    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… 抉择的测试数据的构造是这样的:
    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-itera…

    从后果上看,仿佛劣势非常明显。然而因为只有 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-itera…

    这下劣势就非常明显了。毫无疑问,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 在这个根底上做了循环展开:… // abbreviatedint 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-itera…

     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; // 6long 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-itera…

     如果只有一个字段,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-itera…

    在有 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-itera…

    把字段数量加到了 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-itera…

    对于整数列表的解码,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-itera…

    Protobuf 解决对象列表是 Jackson 的 1.3 倍。然而不迭 DSL-JSON。

    14、对象列表编码性能测试(Encode Object List)

Protobuf 解决对象列表的编码速度是 Jackson 的 2 倍。然而 DSL-JSON 依然比 Protobuf 更快。仿佛 Protobuf 在解决列表的编码解码方面劣势不显著。

15、双精度浮点数数组解码性能测试(Decode Double Array)

Java 的数组有点非凡,double[] 是比 List<Double> 更高效的。应用 double 数组来代表工夫点上的值或者坐标是十分常见的做法。然而,Protobuf 的 Java  库没有提供 double[] 的反对,repeated 总是应用 List<Double>。咱们能够预期 JSON 库在这里有肯定的劣势。message PbTestObject {repeated doublefield1 = 1[packed=true];}https://github.com/json-itera…

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-itera…

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-40…)

正文完
 0