使用 FlatBuffers 提高反序列化性能
最近一直在尋找一個性能和資源占用兼具的序列化和反序列化工具,大多組織都是采用的 JSON, JSON 可以做到數據的前后兼容,并且更容易讓人理解和可視化,但 JSON 的性能相對更差,自身的元數據也會占用更多的存儲空間。
根據官網介紹FlatBuffers是一個高效的、跨平臺的序列化組件,保證數據向前向后兼容性,支持多種編程語言,是專門為游戲開發和其他性能關鍵的應用而開發的。它與Protobuf確實比較相似,最主要的區別就是,FlatBuffers并不需要一個轉換/解包的步驟就可以獲取原數據。
比如在游戲場景下的網絡通信中,玩家往往是對延遲非常敏感的(尤其是在FPS,Moba類游戲中),拋去網絡本身的網絡延遲不談,如果能夠降低數據解析(反序列化)的延遲,就能降低玩家操作的延遲感,提升游戲體驗。
fb 到底能比 pb 快多少?
我自己做了一個測試,結果如下:fb的序列化要略慢于pb的序列化,但是fb的反序列化要遠遠超過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
測試過程很簡單,主要分為序列化和反序列化兩部分,序列化比較簡單,直接使用jmh執行即可;反序列化首先需要把相應序列化的二進制數據寫入文件,靜態讀取二進制文件數據,進行反序列化操作。
pb文件
syntax = "proto2";
package com.test.pb;
option java_outer_classname = "SampleProto";
message Sample {
optional uint32 intData = 1;
// 數據消息
optional uint64 longData = 2;
// string數據
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;
// 數組
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("中國經濟復蘇+" + 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數據
intData:int;
// 數據消息
longData:float;
// string數據
str1:string;
str2:string;
str3:string;
str4:string;
str5:string;
str6:string;
str7:string;
str8:string;
// 數組
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("中國經濟復蘇+" + 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));
}
以上數據生成的二進制文件, pb 大小為 0.763kb,fb 大小為 1.076kb,fb 的存儲占用高出了將近 29%,當然如果是純數字 pb
還會進一步壓縮。
為什么 fb 的反序列化速度這么快?
要搞清楚反序列化快的原因,就得弄明白序列化的過程,因為反序列化是序列化的逆向操作。
FlatBuffers 把對象數據,保存在一個一維的數組中,將數據都緩存在一個 ByteBuffer
中,每個對象在數組中被分為兩部分。元數據部分:負責存放索引。真實數據部分:存放實際的值。然而 FlatBuffers
與大多數內存中的數據結構不同,它使用嚴格的對齊規則和字節順序來確保 buffer 是跨平臺的。此外,對于 table 對象,FlatBuffers
提供前向/后向兼容性和 optional
字段,以支持大多數格式的演變。除了解析效率以外,二進制格式還帶來了另一個優勢,數據的二進制表示通常更具有效率。我們可以使用 4 字節的 UInt 而不是 10
個字符來存儲 10 位數字的整數。
FlatBuffers 對序列化基本使用原則:
- 小端模式。FlatBuffers對各種基本數據的存儲都是按照小端模式來進行的,因為這種模式目前和大部分處理器的存儲模式是一致的,可以加快數據讀寫的數據。
- 寫入數據方向和讀取數據方向不同。
簡單來說, fb 在進行數據序列化的過程中,已經記錄了數據的位置和偏移量。這也是序列化后的數據要略大于 pb 的原因。
FlatBuffers 反序列化的過程就很簡單了。由于序列化的時候保存好了各個字段的 offset,反序列化的過程其實就是把數據從指定的 offset 中讀取出來。
整個反序列化的過程零拷貝,不消耗占用任何內存資源。并且 FlatBuffers 可以讀取任意字段,而不是像 Json 和 Protobuf需要讀取整個對象以后才能獲取某個字段。FlatBuffers 的主要優勢就在反序列化這里了。所以 FlatBuffers可以做到解碼速度極快,或者說無需解碼直接讀取。
總結
FlatBuffers 和 Protobuf 一樣具有數據不可讀性,必須進行數據解析后才能可視化數據。但是相比其它的序列化工具,FlatBuffers最大的優勢是反序列化速度極快,或者說無需解碼。如果使用場景是需要經常解碼序列化的數據,則有可能從 FlatBuffers 的特性中獲得巨大收益。