Protobuf против Flatbuffers против Cap'n Proto, который быстрее? - PullRequest
0 голосов
/ 21 апреля 2020

Я решил выяснить, какие из Protobuf, Flatbuffers и Cap'n proto будут лучшей / самой быстрой сериализацией для моего приложения. В моем случае отправка какого-либо массива байтов / символов по сети (причина, по которой я сериализировал этот формат). Поэтому я сделал простые реализации для всех трех, где я seizeize и dezerialize строка, число с плавающей точкой и int Это дало неожиданные результаты: Protobuf был самым быстрым. Я бы назвал их неожиданными, поскольку и cap'n proto, и flatbuffes «претендуют» на более быстрые варианты. Прежде чем я приму это, я хотел бы посмотреть, не обманул ли я каким-либо образом свой код. Если бы я не обманывал, я хотел бы знать, почему protobuf быстрее (именно поэтому, вероятно, это невозможно). Могут ли эти сообщения быть наравне с cap'n proto и faltbuffers, чтобы они действительно сияли?

Мое время :

Время, затраченное на буферы: 14162 микросекунды
Время, потраченное на capnp: 60259 микросекунд
Время, затраченное на протобуф: 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;
}

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;

}

код protobuf :

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 Ответ

1 голос
/ 22 апреля 2020

В Cap'n Proto вы должны , а не повторно использовать MessageBuilder для нескольких сообщений. То, как вы написали свой код, каждая итерация вашего l oop сделает сообщение больше, потому что вы фактически добавляете к существующему сообщению, а не начинаете новое. Чтобы избежать выделения памяти при каждой итерации, вы должны передать рабочий буфер в конструктор MallocMessageBuilder. Чистый буфер может быть выделен один раз за пределами l oop, но вам нужно каждый раз создавать новый MallocMessageBuilder вокруг l oop. (Конечно, большинство людей не заботятся о чистых буферах и просто позволяют MallocMessageBuilder делать свое собственное распределение, но если вы выберете этот путь в этом тесте, вам также следует изменить тест Protobuf, чтобы создать новый объект сообщения для каждого итерация, а не повторное использование одного объекта.)

Кроме того, ваш код Cap'n Proto использует 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);

Или, чтобы сделать вещи более реалистичными c, вы можете записать сообщение в канал и прочитать его обратно, используя capnp::writeMessageToFd() и capnp::StreamFdMessageReader. (Чтобы быть справедливым, вам нужно было бы сделать так, чтобы тестовый протокол protobuf также записывал / читал из канала.)

(я являюсь автором Cap'n Proto и Protobuf v2. Я не знаком с FlatBuffers, поэтому я не могу комментировать, есть ли в этом коде аналогичные проблемы ...)


О тестах

Я потратил много времени на тестирование Protobuf и Cap ' Прото. Одна вещь, которую я узнал в процессе, состоит в том, что самые простые тесты, которые вы можете создать, не дадут вам реалистичных c результатов.

Во-первых, любой формат сериализации (даже JSON) может "выиграть", учитывая Правильный эталонный случай. Различные форматы будут работать очень и очень по-разному в зависимости от контента. Является ли он строковым, числовым или объектным (то есть с глубокими деревьями сообщений)? Разные форматы здесь имеют разные сильные стороны (например, Cap'n Proto невероятно хорош в числах, потому что он их вообще не трансформирует; JSON невероятно плох в них). Ваш размер сообщения невероятно короткий, средний или очень большой? Короткие сообщения будут в основном использовать код настройки / разрыва, а не обработку тела (но настройка / разборка важна - иногда в реальных случаях использования используется много небольших сообщений!). Очень большие сообщения разрушат кэш L1 / L2 / L3 и расскажут вам больше о пропускной способности памяти, чем сложности анализа (но, опять же, это важно - некоторые реализации более дружественны к кешам, чем другие).

Даже после Учитывая все это, у вас есть еще одна проблема: запуск кода в al oop фактически не говорит вам, как он работает в реальном мире. При работе в узком l oop кэш команд остается горячим, и все ветви становятся очень предсказуемыми. Таким образом, для сериализации с интенсивным ветвлением (например, protobuf) стоимость ветвления будет ограничена, и сериализация с большим объемом кода (опять-таки ... как protobuf) также получит преимущество. Вот почему микро-тесты действительно полезны только для сравнения кода с другими его версиями (например, для тестирования незначительных оптимизаций), а НЕ для сравнения совершенно разных кодовых баз друг с другом. Чтобы выяснить, как все это работает в реальном мире, вам необходимо измерить пример использования в реальном времени. Но ... честно говоря, это довольно сложно. Мало кто успевает построить две версии своего приложения на основе двух разных сериализаций, чтобы узнать, какая из них выиграет ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...