使用 FlatBuffers 提高反序列化性能
最近一直在尋找一個(gè)性能和資源占用兼具的序列化和反序列化工具,大多組織都是采用的 JSON, JSON 可以做到數(shù)據(jù)的前后兼容,并且更容易讓人理解和可視化,但 JSON 的性能相對(duì)更差,自身的元數(shù)據(jù)也會(huì)占用更多的存儲(chǔ)空間。
根據(jù)官網(wǎng)介紹FlatBuffers是一個(gè)高效的、跨平臺(tái)的序列化組件,保證數(shù)據(jù)向前向后兼容性,支持多種編程語言,是專門為游戲開發(fā)和其他性能關(guān)鍵的應(yīng)用而開發(fā)的。它與Protobuf確實(shí)比較相似,最主要的區(qū)別就是,F(xiàn)latBuffers并不需要一個(gè)轉(zhuǎn)換/解包的步驟就可以獲取原數(shù)據(jù)。
比如在游戲場(chǎng)景下的網(wǎng)絡(luò)通信中,玩家往往是對(duì)延遲非常敏感的(尤其是在FPS,Moba類游戲中),拋去網(wǎng)絡(luò)本身的網(wǎng)絡(luò)延遲不談,如果能夠降低數(shù)據(jù)解析(反序列化)的延遲,就能降低玩家操作的延遲感,提升游戲體驗(yàn)。
fb 到底能比 pb 快多少?
我自己做了一個(gè)測(cè)試,結(jié)果如下:fb的序列化要略慢于pb的序列化,但是fb的反序列化要遠(yuǎn)遠(yuǎn)超過pb的反序列化。
Benchmark Mode Cnt Score Error Units
c.s.fb.SampleTest.deserialize thrpt 5 84352854.022 ± 4278679.805 ops/s
c.s.fb.SampleTest.serialize thrpt 5 316259.628 ± 2395.626 ops/s
c.s.pb.SampleTest.deserialize thrpt 5 1407501.471 ± 221477.754 ops/s
c.s.pb.SampleTest.serialize thrpt 5 396038.869 ± 81730.806 ops/s
測(cè)試過程很簡(jiǎn)單,主要分為序列化和反序列化兩部分,序列化比較簡(jiǎn)單,直接使用jmh執(zhí)行即可;反序列化首先需要把相應(yīng)序列化的二進(jìn)制數(shù)據(jù)寫入文件,靜態(tài)讀取二進(jìn)制文件數(shù)據(jù),進(jìn)行反序列化操作。
pb文件
syntax = "proto2";
package com.test.pb;
option java_outer_classname = "SampleProto";
message Sample {
optional uint32 intData = 1;
// 數(shù)據(jù)消息
optional uint64 longData = 2;
// string數(shù)據(jù)
optional string str1 = 3;
optional string str2 = 4;
optional string str3 = 5;
optional string str4 = 6;
optional string str5 = 7;
optional string str6 = 8;
optional string str7 = 9;
optional string str8 = 10;
// 數(shù)組
repeated string person = 11;
}
pb序列化
@Benchmark
public static byte[] serialize() {
SampleProto.Sample.Builder builder = SampleProto.Sample.newBuilder();
List<String> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
list.add("中國經(jīng)濟(jì)復(fù)蘇+" + i);
}
byte[] bytes = builder.setIntData(100).setLongData(System.currentTimeMillis())
.setStr1("306bb851-9a0a-4b07-b22b-7ff49a2a60e1")
.setStr2("306bb851-9a0a-4b07-b22b-7ff49a2a60e2")
.setStr3("306bb851-9a0a-4b07-b22b-7ff49a2a60e3")
.setStr4("306bb851-9a0a-4b07-b22b-7ff49a2a60e4")
.setStr5("306bb851-9a0a-4b07-b22b-7ff49a2a60e5")
.setStr6("306bb851-9a0a-4b07-b22b-7ff49a2a60e6")
.setStr7("306bb851-9a0a-4b07-b22b-7ff49a2a60e7")
.setStr8("306bb851-9a0a-4b07-b22b-7ff49a2a60e8")
.addAllPerson(list).build().toByteArray();
return bytes;
}
pb反序列化
@Benchmark
public static SampleProto.Sample deserialize() throws InvalidProtocolBufferException {
SampleProto.Sample builder = SampleProto.Sample.parseFrom(bytes);
return builder;
}
fb 文件
// 指定生成消息類的Java包
namespace com.test.fb;
// 消息
table Sample {
// int32數(shù)據(jù)
intData:int;
// 數(shù)據(jù)消息
longData:float;
// string數(shù)據(jù)
str1:string;
str2:string;
str3:string;
str4:string;
str5:string;
str6:string;
str7:string;
str8:string;
// 數(shù)組
person:[string];
}
fb序列化
@Benchmark
public static byte[] serialize() {
FlatBufferBuilder flatBufferBuilder = new FlatBufferBuilder();
int str1 = flatBufferBuilder.createString("306bb851-9a0a-4b07-b22b-7ff49a2a60e0");
int str2 = flatBufferBuilder.createString("306bb851-9a0a-4b07-b22b-7ff49a2a60e1");
int str3 = flatBufferBuilder.createString("306bb851-9a0a-4b07-b22b-7ff49a2a60e2");
int str4 = flatBufferBuilder.createString("306bb851-9a0a-4b07-b22b-7ff49a2a60e3");
int str5 = flatBufferBuilder.createString("306bb851-9a0a-4b07-b22b-7ff49a2a60e4");
int str6 = flatBufferBuilder.createString("306bb851-9a0a-4b07-b22b-7ff49a2a60e5");
int str7 = flatBufferBuilder.createString("306bb851-9a0a-4b07-b22b-7ff49a2a60e6");
int str8 = flatBufferBuilder.createString("306bb851-9a0a-4b07-b22b-7ff49a2a60e7");
int[] index = new int[20];
for (int i = 0; i < 20; i++) {
index[i] = flatBufferBuilder.createString("中國經(jīng)濟(jì)復(fù)蘇+" + i);
}
int vectorOfTables = flatBufferBuilder.createVectorOfTables(index);
int sample = Sample.createSample(flatBufferBuilder, 100, System.currentTimeMillis(),
str1, str2, str3, str4, str5, str6, str7, str8, vectorOfTables);
flatBufferBuilder.finish(sample);
return flatBufferBuilder.sizedByteArray();
}
fb反序列化
@Benchmark
public static Sample deserialize() {
return Sample.getRootAsSample(ByteBuffer.wrap(bytes));
}
以上數(shù)據(jù)生成的二進(jìn)制文件, pb 大小為 0.763kb,fb 大小為 1.076kb,fb 的存儲(chǔ)占用高出了將近 29%,當(dāng)然如果是純數(shù)字 pb
還會(huì)進(jìn)一步壓縮。
為什么 fb 的反序列化速度這么快?
要搞清楚反序列化快的原因,就得弄明白序列化的過程,因?yàn)榉葱蛄谢切蛄谢哪嫦虿僮鳌?/p>
FlatBuffers 把對(duì)象數(shù)據(jù),保存在一個(gè)一維的數(shù)組中,將數(shù)據(jù)都緩存在一個(gè) ByteBuffer
中,每個(gè)對(duì)象在數(shù)組中被分為兩部分。元數(shù)據(jù)部分:負(fù)責(zé)存放索引。真實(shí)數(shù)據(jù)部分:存放實(shí)際的值。然而 FlatBuffers
與大多數(shù)內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)不同,它使用嚴(yán)格的對(duì)齊規(guī)則和字節(jié)順序來確保 buffer 是跨平臺(tái)的。此外,對(duì)于 table 對(duì)象,F(xiàn)latBuffers
提供前向/后向兼容性和 optional
字段,以支持大多數(shù)格式的演變。除了解析效率以外,二進(jìn)制格式還帶來了另一個(gè)優(yōu)勢(shì),數(shù)據(jù)的二進(jìn)制表示通常更具有效率。我們可以使用 4 字節(jié)的 UInt 而不是 10
個(gè)字符來存儲(chǔ) 10 位數(shù)字的整數(shù)。
FlatBuffers 對(duì)序列化基本使用原則:
- 小端模式。FlatBuffers對(duì)各種基本數(shù)據(jù)的存儲(chǔ)都是按照小端模式來進(jìn)行的,因?yàn)檫@種模式目前和大部分處理器的存儲(chǔ)模式是一致的,可以加快數(shù)據(jù)讀寫的數(shù)據(jù)。
- 寫入數(shù)據(jù)方向和讀取數(shù)據(jù)方向不同。
簡(jiǎn)單來說, fb 在進(jìn)行數(shù)據(jù)序列化的過程中,已經(jīng)記錄了數(shù)據(jù)的位置和偏移量。這也是序列化后的數(shù)據(jù)要略大于 pb 的原因。
FlatBuffers 反序列化的過程就很簡(jiǎn)單了。由于序列化的時(shí)候保存好了各個(gè)字段的 offset,反序列化的過程其實(shí)就是把數(shù)據(jù)從指定的 offset 中讀取出來。
整個(gè)反序列化的過程零拷貝,不消耗占用任何內(nèi)存資源。并且 FlatBuffers 可以讀取任意字段,而不是像 Json 和 Protobuf需要讀取整個(gè)對(duì)象以后才能獲取某個(gè)字段。FlatBuffers 的主要優(yōu)勢(shì)就在反序列化這里了。所以 FlatBuffers可以做到解碼速度極快,或者說無需解碼直接讀取。
總結(jié)
FlatBuffers 和 Protobuf 一樣具有數(shù)據(jù)不可讀性,必須進(jìn)行數(shù)據(jù)解析后才能可視化數(shù)據(jù)。但是相比其它的序列化工具,F(xiàn)latBuffers最大的優(yōu)勢(shì)是反序列化速度極快,或者說無需解碼。如果使用場(chǎng)景是需要經(jīng)常解碼序列化的數(shù)據(jù),則有可能從 FlatBuffers 的特性中獲得巨大收益。