最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

IM通訊協(xié)議專題學(xué)習(xí)(五):Protobuf到底比JSON快幾倍?全方位實測!

2022-12-16 13:18 作者:nickkckckck  | 我要投稿

本文由陶文分享,InfoQ編輯發(fā)布,有修訂和改動。

1、前言

本系列的前幾篇主要是從各個角度講解Protobuf的基本概念、技術(shù)原理這些內(nèi)容,但回過頭來看,對比JSON這種事實上的數(shù)據(jù)協(xié)議工業(yè)標準,Protobuf到底性能到底高多少?

本篇將以Protobuf為基準,對比市面上的一些主流的JSON解析庫,通過全方位測試來證明給你看看Protobuf到底比JSON快幾倍。

學(xué)習(xí)交流:

- 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM》

- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點此)

(本文已同步發(fā)布于:http://www.52im.net/thread-4095-1-1.html)

2、系列文章

本文是系列文章中的第?5?篇,本系列總目錄如下:

  • 《IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門到精通,一篇就夠!》

  • 《IM通訊協(xié)議專題學(xué)習(xí)(二):快速理解Protobuf的背景、原理、使用、優(yōu)缺點》

  • 《IM通訊協(xié)議專題學(xué)習(xí)(三):由淺入深,從根上理解Protobuf的編解碼原理》

  • 《IM通訊協(xié)議專題學(xué)習(xí)(四):從Base64到Protobuf,詳解Protobuf的數(shù)據(jù)編碼原理》

  • 《IM通訊協(xié)議專題學(xué)習(xí)(五):Protobuf到底比JSON快幾倍?全方位實測!》(* 本文

  • 《IM通訊協(xié)議專題學(xué)習(xí)(六):手把手教你如何在Android上從零使用Protobuf》(稍后發(fā)布..)

  • 《IM通訊協(xié)議專題學(xué)習(xí)(七):手把手教你如何在NodeJS中從零使用Protobuf》(稍后發(fā)布..)

  • 《IM通訊協(xié)議專題學(xué)習(xí)(八):金蝶隨手記團隊的Protobuf應(yīng)用實踐(原理篇) 》(稍后發(fā)布..)

  • 《IM通訊協(xié)議專題學(xué)習(xí)(九):金蝶隨手記團隊的Protobuf應(yīng)用實踐(實戰(zhàn)篇) 》(稍后發(fā)布..)

3、寫在前面

拿 JSON 襯托 Protobuf 的文章真的太多了,經(jīng)常可以看到文章中寫道:“快來用 Protobuf 吧,JSON 太慢啦”。

但是 Protobuf 真的有吹的那么牛么?

我覺得從 JSON 切換到 Protobuf 怎么也得快一倍吧,要不然對不起付出的切換成本。然而,DSL-JSON?的家伙們居然說在Java語言里 JSON 和那些二進制的編解碼格式有得一拼,這太讓人驚訝了!

雖然你可能會說,咱們能不用蘋果和梨來做比較了么?兩個東西根本用途完全不一樣好么。咱們用 Protobuf 是沖著跨語言無歧義的 IDL 的去的,才不僅僅是因為性能呢。好吧,這個我同意。但是仍然有那么多人盲目相信,Protobuf 一定會快很多,我覺得還是有必要徹底終結(jié)一下這個關(guān)于速度的傳說。

DSL-JSON?的博客里只給了他們的測試結(jié)論,但是沒有給出任何原因,以及優(yōu)化的細節(jié),這很難讓人信服數(shù)據(jù)是真實的。你要說 JSON 比二進制格式更快,真的是很反直覺的事情。

稍微琢磨一下這個問題,就可以列出好幾個 Protobuf 應(yīng)該更快的理由。

比如:

  • 1)更容容易綁定值到對象的字段上。JSON 的字段是用字符串指定的,相比之下字符串比對應(yīng)該比基于數(shù)字的字段tag更耗時;

  • 2)JSON 是文本的格式,整數(shù)和浮點數(shù)應(yīng)該更占空間而且更費時;

  • 3)Protobuf 在正文前有一個大小或者長度的標記,而 JSON 必須全文掃描無法跳過不需要的字段。

但是僅憑這幾點是不是就可以蓋棺定論了呢?未必。

也有相反的觀點:

  • 1)如果字段大部分是字符串,占到?jīng)Q定性因素的因素可能是字符串拷貝的速度,而不是解析的速度。在這個評測中,我們看到不少庫的性能是非常接近的。這是因為測試數(shù)據(jù)中大部分是由字符串構(gòu)成的;

  • 2)影響解析速度的決定性因素是分支的數(shù)量。因為分支的存在,解析仍然是一個本質(zhì)上串行的過程。雖然Protobuf里沒有[] 或者 {},但是仍然有類似的分支代碼的存在。如果沒有這些分支的存在,解析不過就是一個 memcpy 的操作而已。只有 Parabix 這樣的技術(shù)才有革命性的意義,而 Protobuf 相比 JSON 只是改良而非革命;

  • 3)也許 Protobuf 是一個理論上更快的格式,但是實現(xiàn)它的庫并不一定就更快。這取決于優(yōu)化做得好不好,如果有不必要的內(nèi)存分配或者重復(fù)讀取,實際的速度未必就快。

有多個 benchmark 都把 DSL-JSON列到前三名里,有時甚至比其他的二進制編碼更快。

經(jīng)過我仔細分析,原因出在了這些 benchmark 對于測試數(shù)據(jù)的構(gòu)成選擇上。

因為構(gòu)造測試數(shù)據(jù)很麻煩,所以一般評測只會對相同的測試數(shù)據(jù),去測不同的庫的實現(xiàn)。

這樣就使得結(jié)果是嚴重傾向于某種類型輸入的。

比如?https://github.com/eishay/jvm-serializers/wiki?選擇的測試數(shù)據(jù)的結(jié)構(gòu)是這樣的:

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;

}

無論怎么去構(gòu)造?small/medium/large?的輸入,benchmark 仍然是存在特定傾向性的。

而且這種傾向性是不明確的。比如 medium 的輸入,到底說明了什么?medium 對于不同的人來說,可能意味著完全不同的東西。

所以,在這里我想改變一下游戲的規(guī)則。不去選擇一個所謂的最現(xiàn)實的配比,而是構(gòu)造一些極端的情況。

這樣,我們可以一目了然的知道,JSON的強項和弱點都是什么。通過把這些缺陷放大出來,我們也就可以對最壞的情況有一個清晰的預(yù)期。具體在你的場景下性能差距是怎樣的一個區(qū)間內(nèi),也可以大概預(yù)估出來。

4、本次評測對象

好了,廢話不多說了,JMH 擼起來。

benchmark 的對象有以下幾個:

  • 1)Jackson:Java 程序里用的最多的 JSON 解析器。benchmark 中開啟了 AfterBurner 的加速特性;

  • 2)DSL-JSON:世界上最快的 Java JSON 實現(xiàn);

  • 3)Jsoniter:抄襲 DSL-JSON 寫的實現(xiàn);

  • 4)Fastjson:在中國很流行的 JSON 解析器;

  • 5)Protobuf:在 RPC (遠程方法調(diào)用)里非常流行的二進制編解碼格式;

  • 6)Thrift:另外一個很流行的 RPC 編解碼格式。這里 benchmark 的是 TCompactProtocol。

5、整數(shù)解碼性能測試(Decode Integer)

先從一個簡單的場景入手。

毫無疑問,Protobuf 非常擅長于處理整數(shù):

message PbTestObject {

??int32 field1 = 1;

}

https://github.com/json-iterator/java-benchmark/tree/master/src/main/java/com/jsoniter/benchmark/with_int

從結(jié)果上看,似乎優(yōu)勢非常明顯。但是因為只有 1 個整數(shù)字段,所以可能整數(shù)解析的成本沒有占到大頭。

所以,我們把測試調(diào)整對象調(diào)整為 10 個整數(shù)字段。再比比看:

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

這下優(yōu)勢就非常明顯了。毫無疑問,Protobuf 解析整數(shù)的速度是非??斓?,能夠達到 Jackson 的 8 倍。

DSL-JSON 比 Jackson 快很多,它的優(yōu)化代碼在這里:

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;

}

整數(shù)是直接從輸入的字節(jié)里計算出來的,公式是?value = (value << 3) + (value << 1) + ind;?相比讀出字符串,然后調(diào)用 Integer.valueOf ,這個實現(xiàn)只遍歷了一遍輸入,同時也避免了內(nèi)存分配。

Jsoniter 在這個基礎(chǔ)上做了循環(huán)展開:

... // 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、整數(shù)編碼性能測試(Encode Integer)

編碼方面情況如何呢?和編碼一樣的測試數(shù)據(jù),測試結(jié)果如下:

不知道為啥,Thrift 的序列化特別慢。而且別的 benchmark 里 Thrift 的序列化都是算慢的。我猜測應(yīng)該是實現(xiàn)里有不夠優(yōu)化的地方吧,格式應(yīng)該沒問題。整數(shù)編碼方面,Protobuf 是 Jackson 的 3 倍。但是和 DSL-JSON 比起來,好像沒有快很多。

這是因為 DSL-JSON 使用了自己的優(yōu)化方式,和 JDK 的官方實現(xiàn)不一樣(代碼點此查看):

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;

}

這段代碼的意思是比較令人費解的。不知道哪里就做了數(shù)字到字符串的轉(zhuǎn)換了。

過程是這樣的,假設(shè)輸入了19823,會被分解為 19 和 823 兩部分。然后有一個 `DIGITS` 的查找表,根據(jù)這個表把 19 翻譯為 "19",把 823 翻譯為 "823"。其中 "823" 并不是三個byte分開來存的,而是把bit放到了一個integer里,然后在 writeBuf 的時候通過位移把對應(yīng)的三個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;

}

這個實現(xiàn)比 JDK 自帶的 Integer.toString 更快。因為查找表預(yù)先計算好了,節(jié)省了運行時的計算成本。

7、雙精度浮點數(shù)解碼性能測試(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真的不適合存浮點數(shù)。

DSL-Json 中對 Double 也是做了特別優(yōu)化的(詳見源碼):

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;

}

浮點數(shù)被去掉了點,存成了 long 類型,然后再除以對應(yīng)的10的倍數(shù)。如果輸入是3.1415,則會變成 31415/10000。

8、雙精度浮點數(shù)編碼性能測試(Encode Double)

把 double 編碼為文本格式就更困難了。

解碼 double 的時候,Protobuf 是 Jackson 的13 倍。如果你愿意犧牲精度的話,Jsoniter?可以選擇只保留6位小數(shù)。在這個取舍下,可以好一些,但是 Protobuf 仍然是Jsoniter?的兩倍。

保留6位小數(shù)的代碼是這樣寫的,把 double 的處理變成了長整數(shù)的處理:

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 不是為數(shù)字設(shè)計的。如果你使用的是 Jackson,切換到 Protobuf 的話可以把數(shù)字的處理速度提高 10 倍。然而 DSL-Json 做的優(yōu)化可以把這個性能差距大幅縮小,解碼在 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)

我們已經(jīng)看到了 JSON 在處理數(shù)字方面的笨拙丑態(tài)了。在處理對象綁定方面,是不是也一樣不堪?

前面的 benchmark 結(jié)果那么差和按字段做綁定是不是有關(guān)系?畢竟我們有 10 個字段要處理那。這就來看看在處理字段方面的效率問題。

為了讓比較起來公平一些,我們使用很短的 ascii 編碼的字符串作為字段的值。這樣字符串拷貝的成本大家都差不到哪里去。

所以性能上要有差距,必然是和按字段綁定值有關(guān)系。

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 要慢。

我們再把同樣的實驗重復(fù)幾次,分別對應(yīng) 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

把字段數(shù)量加到了 10 個之后,Protobuf 僅僅是 Jackson 的 1.22 倍了??吹竭@里,你應(yīng)該懂了吧。

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;

????}

??}

}

這個實現(xiàn)比 Hashmap 來說,僅僅是稍微略快而已。

DSL-JSON 的實現(xiàn)是先 hash,然后也是類似的分發(fā)的方式:

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,它可以產(chǎn)生下面這樣的嚴格匹配的代碼:

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 檢查。

關(guān)于對象綁定來說,只要字段名不長,基于數(shù)字的 tag 分發(fā)并不會比 JSON 具有明顯優(yōu)勢,即便是相比最慢的 Jackson 來說也是如此。

10、對象編碼性能測試(Encode Object)

廢話不多說了,直接比較一下三種字段數(shù)量情況下,編碼的速度。

只有 1 個字段:

有 5 個字段:

有 10 個字段:

對象編碼方面,Protobuf 是 Jackson 的 1.7 倍。但是速度其實比 DSL-Json 還要慢。

優(yōu)化對象編碼的方式是,一次性盡可能多的把控制類的字節(jié)寫出去。

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 結(jié)果可以看出,Protobuf 在這個方面僅僅比 Jackson 略微強一些,而比 DSL-Json 要慢。

11、整形列表解碼性能測試(Decode Integer List)

Protobuf 對于整數(shù)列表有特別的支持,可以打包存儲:

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)

設(shè)置 [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

對于整數(shù)列表的解碼,Protobuf 是 Jackson 的 3 倍。然而比 DSL-Json 的優(yōu)勢并不明顯。

在 Jsoniter 里,解碼的循環(huán)被展開了:

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;

}

對于成員比較少的情況,這樣搞可以避免數(shù)組的擴容帶來的內(nèi)存拷貝。

12、整形列表編碼性能測試(Encode Integer List)

Protobuf 在編碼數(shù)組的時候應(yīng)該有優(yōu)勢,不用寫那么多逗號出來嘛。

Protobuf 在編碼整數(shù)列表的時候,僅僅是 Jackson 的 1.35 倍。

雖然 Protobuf 在處理對象的整數(shù)字段的時候優(yōu)勢明顯,但是在處理整數(shù)的列表時卻不是如此。在這個方面,DSL-Json 沒有特殊的優(yōu)化,性能的提高純粹只是因為單個數(shù)字的編碼速度提高了。

13、對象列表解碼性能測試(Decode Object List)

列表經(jīng)常用做對象的容器。測試這種兩種容器組合嵌套的場景,也很有代表意義。

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 在處理列表的編碼解碼方面優(yōu)勢不明顯。

15、雙精度浮點數(shù)數(shù)組解碼性能測試(Decode Double Array)

Java 的數(shù)組有點特殊,double[] 是比 List<Double> 更高效的。使用 double 數(shù)組來代表時間點上的值或者坐標是非常常見的做法。

然而,Protobuf 的 Java??庫沒有提供double[] 的支持,repeated 總是使用 List<Double>。我們可以預(yù)期 JSON 庫在這里有一定的優(yōu)勢。

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 數(shù)組方面,Jackson 與之的差距被縮小為 5 倍。Protobuf 與 DSL-JSON 相比,優(yōu)勢已經(jīng)不明顯了。所以如果你有很多的 double 數(shù)值需要處理,這些數(shù)值必須是在對象的字段上,才會引起性能的巨大差別,對于數(shù)組里的 double,優(yōu)勢差距被縮小。

在 Jsoniter 里,處理數(shù)組的循環(huán)也是被展開的。

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;

}

這避免了數(shù)組擴容的開銷。

16、雙精度浮點數(shù)數(shù)組編碼性能測試(Encode Double Array)

再來看看 double 數(shù)組的編碼:

?

Protobuf 可以飛快地對 double 數(shù)組進行編碼,是 Jackson 的 15 倍。在犧牲精度的情況下,Protobuf 只是Jsoniter 的 2.3 倍。

所以,再次證明了,JSON 處理 double 非常慢。如果用 base64 編碼 double,則可以保持精度,速度和犧牲精度時一樣。

17、字符串解碼性能測試(Decode String)

JSON 字符串包含了轉(zhuǎn)義字符的支持。Protobuf 解碼字符串僅僅是一個內(nèi)存拷貝。理應(yīng)更快才對。被測試的字符串長度是 160 個字節(jié)的 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 的處理負擔(dān)更重,為什么會更快呢?

先嘗試捷徑:

DSL-JSON 給 ascii 實現(xiàn)了一個捷徑(源碼點此):

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;

}

這個捷徑里規(guī)避了處理轉(zhuǎn)義字符和utf8字符串的成本。

JVM 的動態(tài)編譯做了特殊優(yōu)化:

在 JDK9 之前,java.lang.String 都是基于 `char[]` 的。而輸入都是 byte[] 并且是 utf-8 編碼的。所以這使得,我們不能直接用 memcpy 的方式來處理字符串的解碼問題。

但是在 JDK9 里,java.lang.String 已經(jīng)改成了基于`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;

????}

}

使用這個雖然被廢棄,但是還沒有被刪除的構(gòu)造函數(shù),我們可以使用 Arrays.copyOfRange 來直接構(gòu)造 java.lang.String 了。然而,在測試之后,發(fā)現(xiàn)這個實現(xiàn)方式并沒有比 DSL-JSON 的實現(xiàn)更快。

似乎 JVM 的 Hotspot 動態(tài)編譯時對這段循環(huán)的代碼做了模式匹配,識別出了更高效的實現(xiàn)方式。即便是在 JDK9 使用 +UseCompactStrings 的前提下,理論上來說本應(yīng)該更慢的?byte[] => char[] => byte[]?并沒有使得這段代碼變慢,DSL-JSON 的實現(xiàn)還是最快的。

如果輸入大部分是字符串,這個優(yōu)化就變得至關(guān)重要了。Java 里的解析藝術(shù),還不如說是字節(jié)拷貝的藝術(shù)。JVM 的 java.lang.String 設(shè)計實在是太愚蠢了。在現(xiàn)代一點的語言中,比如 Go,字符串都是基于 utf-8 byte[] 的。

18、字符串編碼性能測試(Encode String)

類似的問題,因為需要把 char[] 轉(zhuǎn)換為 byte[],所以沒法直接內(nèi)存拷貝。

Protobuf 在編碼長字符串時,比 Jackson 略微快一點點,一切都歸咎于 char[]。

19、本文總結(jié)

最后,我們把所有的戰(zhàn)果匯總到一起。

?

編解碼數(shù)字的時候,JSON仍然是非常慢的。Jsoniter 把這個差距從 10 倍縮小到了 3 倍多一些。

JSON 最差的情況是下面幾種:

  • 1)跳過非常長的字符串:和字符串長度線性相關(guān);

  • 2)解碼 double 字段:Protobuf 優(yōu)勢明顯,是 Jsoniter的 3.27 倍,是 Jackson 的 13.75 倍;

  • 3)編碼 double 字段:如果不能接受只保留 6 位小數(shù),Protobuf 是 Jackson 的 12.71 倍(如果接受精度損失,Protobuf 是 Jsoniter 的 1.96 倍);

  • 4)解碼整數(shù):Protobuf 是 Jsoniter 的 2.64 倍,是 Jackson 的 8.51 倍。

如果你的生產(chǎn)環(huán)境中的JSON沒有那么多的double字段,都是字符串占大頭,那么基本上來說替換成 Protobuf 也就是僅僅比 Jsoniter 提高一點點,肯定在2倍之內(nèi)。如果不幸的話,沒準 Protobuf 還要更慢一點。

20、參考資料

[1]?Protobuf官方編碼資料

[2]?Protobuf官方手冊

[3]?Why do we use Base64?

[4]?The Base16, Base32, and Base64 Data Encodings

[5]?Protobuf從入門到精通,一篇就夠!

[5]?如何選擇即時通訊應(yīng)用的數(shù)據(jù)傳輸格式

[7]?強列建議將Protobuf作為你的即時通訊應(yīng)用數(shù)據(jù)傳輸格式

[8]?APP與后臺通信數(shù)據(jù)格式的演進:從文本協(xié)議到二進制協(xié)議

[9]?面試必考,史上最通俗大小端字節(jié)序詳解

[10]?移動端IM開發(fā)需要面對的技術(shù)問題(含通信協(xié)議選擇)

[11]?簡述移動端IM開發(fā)的那些坑:架構(gòu)設(shè)計、通信協(xié)議和客戶端

[12]?理論聯(lián)系實際:一套典型的IM通信協(xié)議設(shè)計詳解

[13]?58到家實時消息系統(tǒng)的協(xié)議設(shè)計等技術(shù)實踐分享

(本文已同步發(fā)布于:http://www.52im.net/thread-4095-1-1.html)


IM通訊協(xié)議專題學(xué)習(xí)(五):Protobuf到底比JSON快幾倍?全方位實測!的評論 (共 條)

分享到微博請遵守國家法律
双柏县| 黄陵县| 华亭县| 咸丰县| 巴彦淖尔市| 灵武市| 绥阳县| 绍兴市| 汉阴县| 崇义县| 宣城市| 杂多县| 攀枝花市| 新邵县| 肥城市| 溧水县| 铜鼓县| 汉沽区| 突泉县| 沙田区| 灵璧县| 九江县| 收藏| 习水县| 普格县| 恭城| 满洲里市| 泾阳县| 宁武县| 左贡县| 海淀区| 雅江县| 江安县| 竹北市| 临西县| 民权县| 离岛区| 沙坪坝区| 贵德县| 洛浦县| 天峻县|