我决定找出Protobuf、Flatbuffers和Cap 'n proto中哪一个对我的应用程序来说是最好/最快的序列化。在我的例子中,我通过网络发送某种字节/字符数组(这是我序列化为那种格式的原因)。所以我对这三个都做了简单的实现,我对字符串、浮点数和整型进行了序列化和去零化。这产生了意想不到的结果:Protobuf是最快的。我认为它们是出乎意料的,因为cap 'n proto和flatbuffes都“声称”是更快的选项。在我接受这个说法之前,我想看看我是否故意在代码中作弊。如果我没有作弊,我想知道为什么protobuf更快(确切的原因可能是不可能的)。消息是否可以简化cap' n proto和faltbuffes,以真正使它们发光?
我的计时:
平面缓冲区花费的时间:14162微秒
所用时间capnp:60259微秒
protobuf花费的时间:12131微秒
(time相对比较可能是相关的。)
- 更新:以上数字不代表正确的用法,至少不代表capnp --见答案和评论。*
平面缓冲区代码:
int main (int argc, char *argv[]){
std::string s = "string";
float f = 3.14;
int i = 1337;
std::string s_r;
float f_r;
int i_r;
flatbuffers::FlatBufferBuilder message_sender;
int steps = 10000;
auto start = high_resolution_clock::now();
for (int j = 0; j < steps; j++){
auto autostring = message_sender.CreateString(s);
auto encoded_message = CreateTestmessage(message_sender, autostring, f, i);
message_sender.Finish(encoded_message);
uint8_t *buf = message_sender.GetBufferPointer();
int size = message_sender.GetSize();
message_sender.Clear();
//Send stuffs
//Receive stuffs
auto recieved_message = GetTestmessage(buf);
s_r = recieved_message->string_()->str();
f_r = recieved_message->float_();
i_r = recieved_message->int_();
}
auto stop = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(stop - start);
cout << "Time taken flatbuffer: " << duration.count() << " microseconds" << endl;
return 0;
}
cap 'n原型代码:
int main (int argc, char *argv[]){
char s[] = "string";
float f = 3.14;
int i = 1337;
const char * s_r;
float f_r;
int i_r;
::capnp::MallocMessageBuilder message_builder;
Testmessage::Builder message = message_builder.initRoot<Testmessage>();
int steps = 10000;
auto start = high_resolution_clock::now();
for (int j = 0; j < steps; j++){
//Encodeing
message.setString(s);
message.setFloat(f);
message.setInt(i);
kj::Array<capnp::word> encoded_array = capnp::messageToFlatArray(message_builder);
kj::ArrayPtr<char> encoded_array_ptr = encoded_array.asChars();
char * encoded_char_array = encoded_array_ptr.begin();
size_t size = encoded_array_ptr.size();
//Send stuffs
//Receive stuffs
//Decodeing
kj::ArrayPtr<capnp::word> received_array = kj::ArrayPtr<capnp::word>(reinterpret_cast<capnp::word*>(encoded_char_array), size/sizeof(capnp::word));
::capnp::FlatArrayMessageReader message_receiver_builder(received_array);
Testmessage::Reader message_receiver = message_receiver_builder.getRoot<Testmessage>();
s_r = message_receiver.getString().cStr();
f_r = message_receiver.getFloat();
i_r = message_receiver.getInt();
}
auto stop = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(stop - start);
cout << "Time taken capnp: " << duration.count() << " microseconds" << endl;
return 0;
}
协议缓冲区代码:
int main (int argc, char *argv[]){
std::string s = "string";
float f = 3.14;
int i = 1337;
std::string s_r;
float f_r;
int i_r;
Testmessage message_sender;
Testmessage message_receiver;
int steps = 10000;
auto start = high_resolution_clock::now();
for (int j = 0; j < steps; j++){
message_sender.set_string(s);
message_sender.set_float_m(f);
message_sender.set_int_m(i);
int len = message_sender.ByteSize();
char encoded_message[len];
message_sender.SerializeToArray(encoded_message, len);
message_sender.Clear();
//Send stuffs
//Receive stuffs
message_receiver.ParseFromArray(encoded_message, len);
s_r = message_receiver.string();
f_r = message_receiver.float_m();
i_r = message_receiver.int_m();
message_receiver.Clear();
}
auto stop = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(stop - start);
cout << "Time taken protobuf: " << duration.count() << " microseconds" << endl;
return 0;
}
不包括消息定义文件,因为它们很简单,很可能与此无关。
1条答案
按热度按时间ee7vknir1#
在Cap 'n Proto中,你不应该为多条消息重用
MessageBuilder
。按照你编写代码的方式,循环的每次迭代都会使消息变大,因为你实际上是在现有消息上添加内容,而不是开始一条新消息。为了避免每次迭代都分配内存,你应该传递一个暂存缓冲区给MallocMessageBuilder
的构造函数。2暂存缓冲区可以在循环外分配一次,但是你需要在每次循环时创建一个新的MallocMessageBuilder
。(当然,大多数人并不关心暂存缓冲区,而是让MallocMessageBuilder
自己分配,但是如果您在此基准测试中选择了这条路径,那么您还应该更改Protobuf基准测试,以便为每次迭代创建一个新的消息对象,而不是重用单个对象。)另外,您的Cap 'n Proto代码使用
capnp::messageToFlatArray()
,它分配了一个全新的缓冲区来放置消息并复制整个消息。这不是使用Cap' n Proto的最有效方式。通常,如果您要将消息写入文件或套接字,您将直接从消息的原始后备缓冲区写入,而不进行此复制。请尝试这样做:或者,为了使事情更实际一些,您可以使用
capnp::writeMessageToFd()
和capnp::StreamFdMessageReader
将消息写出到一个管道中,然后再将其读回(公平地说,您还需要使protobuf基准测试写入/读取管道)。(我是Cap 'n Proto和Protobuf v2的作者。我不熟悉FlatBuffers,所以我不能评论该代码是否有任何类似的问题...)
性能指标评测
我花了很多时间对Protobuf和Cap 'n Proto进行基准测试,在这个过程中我学到了一件事,那就是你所能创建的最简单的基准测试不会给予你实际的结果。
首先,任何序列化格式(甚至JSON)都可以在正确的基准测试中“胜出”。不同的格式根据内容的不同表现得非常、非常不同。是字符串型、数字型还是对象型(即具有深度消息树)?不同的格式在这里有不同的优势(例如,Cap 'n Proto非常擅长数字,因为它根本不转换数字; JSON在这方面做得非常糟糕)。您的消息大小是非常短、中等长度还是非常大?短消息将主要执行setup/teardown代码,而不是正文处理(但是setup/teardown很重要--有时候真实世界的用例涉及到大量的小消息!)。非常大的消息将破坏L1/L2/L3缓存,并告诉您更多关于内存带宽的信息,而不是解析复杂度(但是,这一点很重要--某些实现比其他实现更适合缓存)。
即使在考虑了所有这些之后,你还有另一个问题:在循环中运行代码实际上并不能告诉你它在真实的世界中是如何执行的。当在一个紧密循环中运行时,指令缓存保持热状态,所有的分支都变得高度可预测。因此,一个分支密集的序列化(如protobuf)的分支开销被掩盖起来,和代码占用量大的序列化这就是为什么微基准测试只在将代码与其自身的其他版本进行比较时才真正有用(例如,测试次要优化),而不是比较完全不同的代码库。要了解这些代码库在真实的世界中的表现,您需要端到端地衡量现实世界的用例。但是......老实说,这相当困难。很少有人有时间基于两个不同的序列化构建整个应用的两个版本,以确定哪一个版本胜出......