C++ Protobuf vs扁平缓冲器vs Cap';n proto哪个更快?

C++ Protobuf vs扁平缓冲器vs Cap';n proto哪个更快?,c++,protocol-buffers,flatbuffers,capnproto,C++,Protocol Buffers,Flatbuffers,Capnproto,我决定找出Protobuf、Flatbuffers和Cap'n proto中的哪一个是我的应用程序最好/最快的序列化。在我的例子中,通过网络发送某种字节/字符数组(这就是我序列化为这种格式的原因)。因此,我对这三种方法都做了简单的实现,其中我对字符串、浮点和int进行了sealize和dezeralize。这带来了意想不到的结果:Protobuf是最快的。我会说他们出人意料,因为cap'n proto和flatbuffes都“声称”是更快的选择。在我接受这一点之前,我想看看我是否以某种方式在代码

我决定找出Protobuf、Flatbuffers和Cap'n proto中的哪一个是我的应用程序最好/最快的序列化。在我的例子中,通过网络发送某种字节/字符数组(这就是我序列化为这种格式的原因)。因此,我对这三种方法都做了简单的实现,其中我对字符串、浮点和int进行了sealize和dezeralize。这带来了意想不到的结果:Protobuf是最快的。我会说他们出人意料,因为cap'n proto和flatbuffes都“声称”是更快的选择。在我接受这一点之前,我想看看我是否以某种方式在代码中进行了常规作弊。如果我没有作弊,我想知道protobuf为什么更快(确切的原因可能是不可能的)。这些信息会不会是为了模仿普罗托船长和福特布弗船长,让他们真正发光

我的时间安排

所用时间:14162微秒
capnp所用时间:60259微秒
protobuf所用时间:12131微秒
(显然,这取决于我的机器,但重要的是相对时间)

扁平缓冲区代码

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;
}

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;
}

intmain(intargc,char*argv[]){
std::string s=“string”;
浮点数f=3.14;
int i=1337;
std::字符串s_r;
浮球;
国际情报局;
flatbuffers::FlatBufferBuilder消息\u发送方;
int步数=10000;
自动启动=高分辨率时钟::现在();
对于(int j=0;j字符串_()->str();
f_r=收到的_消息->浮动_();
i_r=收到消息->输入();
}
自动停止=高分辨率时钟::现在();
自动持续时间=持续时间(停止-启动);

cout在Cap'n Proto中,您不应该对多条消息重复使用
MessageBuilder
。按照您编写代码的方式,循环的每次迭代都会使消息变大,因为您实际上是在添加现有消息,而不是开始新消息。为了避免每次迭代都分配内存,您应该传递一个sc将缓冲区棘轮到
MallocMessageBuilder
的构造函数。可以在循环外分配一次临时缓冲区,但每次循环时都需要创建一个新的
MallocMessageBuilder
。(当然,大多数人不需要临时缓冲区,只需要让
MallocMessageBuilder
自己分配,但是如果您在这个基准中选择了这个路径,那么您还应该更改Protobuf基准,为每个迭代创建一个新的消息对象,而不是重用单个对象。)

此外,您的Cap'n协议代码正在使用
capnp::messageToFlatArray()
,它分配一个全新的缓冲区,将消息放入其中并复制整个消息。这不是使用Cap'n Proto的最有效方法。通常,如果您将消息写入文件或套接字,您将直接从消息的原始备份缓冲区写入,而不进行此复制。请尝试执行以下操作:

kj::ArrayPtr<const kj::ArrayPtr<const capnp::word>> segments =
    message_builder.getSegmentsForOutput();

// Send segments
// Receive segments

capnp::SegmentArrayMessageReader message_receiver_builder(segments);
kj::ArrayPtr段=
message_builder.getSegmentsFroutput();
//发送段
//接收段
capnp::分段数组消息阅读器消息接收器生成器(分段);
或者,为了使事情更加真实,您可以将消息写入管道,然后使用
capnp::writeMessageOfd()
capnp::StreamFdMessageReader
将其读回(公平地说,您还需要使protobuf基准向管道写入/从管道读取)

(我是Cap'n Proto和Protobuf v2的作者。我不熟悉FlatBuffers,因此我无法评论该代码是否存在任何类似的问题…)


基准 我花了很多时间对Protobuf和Cap'n Proto进行基准测试。在这个过程中我学到的一件事是,您可以创建的大多数简单基准都不会给您带来实际的结果

首先,任何序列化格式(即使是JSON)都可以在正确的基准情况下“获胜”。不同的格式将根据内容执行非常非常不同的操作。它是字符串重、数字重还是对象重(即具有深层消息树)?不同的格式在这里有不同的优势(例如,Cap'n Proto非常擅长数字,因为它根本不转换数字;JSON非常不擅长数字)。您的消息大小是非常短、中等长度还是非常大?短消息主要是执行设置/拆卸代码,而不是正文处理(但设置/拆卸很重要——有时现实世界的用例涉及大量小消息!)。非常大的消息会破坏一级/二级/三级缓存,并告诉您更多关于内存带宽的信息,而不是解析复杂性(但同样重要的是,有些实现比其他实现更容易缓存)

即使考虑了所有这些因素,您也会遇到另一个问题:在循环中运行代码实际上并不能告诉您它在现实世界中的执行情况。在紧密循环中运行时,指令缓存保持热状态,所有分支都变得高度可预测。因此,分支密集型序列化(如protobuf)它的分支成本将被掩盖,而代码占用量大的序列化(同样……像protobuf)也将获得优势。这就是为什么微基准测试只在将代码与其他版本本身进行比较时才真正有用(例如,测试较小的优化),而不是相互比较完全不同的代码基。要了解这些代码基在现实世界中的表现,您需要对真实世界的用例进行端到端的测量。但是……老实说,这相当困难。Fe