首页 > 编程语言 >Protobuf 为什么这么快?解密它背后的高效编码机制与 C++ 实践

Protobuf 为什么这么快?解密它背后的高效编码机制与 C++ 实践

时间:2024-10-01 13:49:26浏览次数:9  
标签:std 编码 Protobuf 二进制 解密 C++ Person 序列化

目录


在如今的数据密集型应用中,数据传输的效率往往直接影响系统的性能。Protocol Buffers(简称 Protobuf)作为 Google 推出的高效数据序列化协议,因其紧凑的二进制编码方式和高速的解析能力,成为了众多高性能系统的首选。

你是否曾好奇,为什么 Protobuf 的序列化与反序列化速度可以远超其他格式,如 JSON 和 XML?本文将带你从编码原理C++ 实践,逐步解析 Protobuf 高效的背后原因。

本文重点将覆盖以下几个方面:

  1. Protobuf 的基本使用:带你从零开始构建一个简单的 Person 消息。
  2. Protobuf 的二进制编码机制:深入解析 Protobuf 如何通过 Varint 编码等机制提升序列化速度。
  3. C++ 代码实践:通过具体的序列化与反序列化代码,展示其在实际项目中的应用。
  4. 性能对比与优化:对比 Protobuf 与其他数据格式的性能表现,并探讨如何进一步优化。
  5. 示意图解读:通过详细的示意图,帮助你更好地理解 Protobuf 的编码流程。

如果你想提升系统的性能,深入了解 Protobuf 是一个绝佳的起点。接下来,让我们从最基础的 Protobuf 使用开始,一步步揭开它的高效秘密。


1. Protobuf 的基本使用

在深入探讨 Protobuf 的性能优势之前,首先需要掌握如何定义和使用 Protobuf 的数据结构。以下以 Person 消息的定义为例,展示 Protobuf 的基本用法。

1.1 定义 .proto 文件

创建 person.proto 文件,定义一个简单的 Person 消息:

syntax = "proto3";

message Person {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

解析:

  • syntax = “proto3”;:使用 Protobuf 3 语法,兼容性好,且支持更多现代功能。
  • message Person:定义了一个名为 Person 的消息类型,该类型包含了三个字段:idnameemail
  • 字段定义:每个字段都包含数据类型(如 int32string)、名称(idnameemail)以及唯一的编号。字段编号在后续的二进制编码中非常重要,直接决定数据的解析顺序。
1.2 生成 C++ 代码

接下来,使用 protoc 工具将 .proto 文件编译成 C++ 代码:

protoc --cpp_out=. person.proto

这条命令会生成两个文件:person.pb.hperson.pb.cc,分别包含 Person 类的定义和实现。接下来我们将通过 C++ 代码来演示如何使用这些生成的文件进行序列化和反序列化。

2. Protobuf 的二进制编码机制

Protobuf 之所以具备高效的性能,其核心在于它的紧凑二进制编码,这显著减少了数据的体积,进而加快了数据的传输与处理速度。以下将通过几个关键技术点深入解析 Protobuf 的编码机制。

2.1 Varint 编码:更少的字节,更高的效率

Protobuf 使用 Varint(可变长度整数)编码来表示整数类型。与传统的定长整数不同,Varint 使用较少的字节来表示较小的整数。这种方式不仅节省了空间,也提升了解析效率。

示例:

假设有一个数值 300,使用 Varint 编码如下:

Number: 300
Varint Encoding: 0xAC 0x02

解释:
300 = 0b1 0010 1100
分成 7 位:0b0010 1100 (44), 0b0000 0010 (2)
最高位(MSB)表示是否有后续字节:
0xAC = 0b10101100
0x02 = 0b00000010

通过这种方式,Protobuf 能够将整数值编码为可变长度,从而减少了高频数据的字节数。这在处理大量小数值时,优势尤为明显。

2.2 字段编号与键:精准定位每个数据

Protobuf 的每个字段在编码时不仅仅是值,还包含一个 ,该键由字段编号和数据类型编码方式(即 wire_type)组合而成。字段编号和 wire_type 的组合使得 Protobuf 在二进制流中能够高效地识别并解析每个字段。

键的组成:

key = (field_number << 3) | wire_type

通过这种设计,Protobuf 可以在编码时将数据结构紧凑排列,并且在反序列化时快速定位字段。

3. C++ 序列化与反序列化示例

接下来,我们通过 C++ 代码来展示如何在实际应用中使用 Protobuf 进行数据序列化与反序列化。以下是详细的代码示例:

3.1 序列化示例
#include "person.pb.h"
#include <iostream>
#include <fstream>

void SerializePerson(const std::string& filename) {
    // 创建一个 Person 对象并赋值
    Person person;
    person.set_id(123);
    person.set_name("Alice");
    person.set_email("[email protected]");

    // 将对象序列化到文件中
    std::ofstream output(filename, std::ios::binary);
    if (!person.SerializeToOstream(&output)) {
        std::cerr << "Failed to write person." << std::endl;
    }
}
3.2 反序列化示例
void DeserializePerson(const std::string& filename) {
    // 创建一个空的 Person 对象
    Person person;

    // 从文件中反序列化数据
    std::ifstream input(filename, std::ios::binary);
    if (!person.ParseFromIstream(&input)) {
        std::cerr << "Failed to parse person." << std::endl;
    }

    // 输出反序列化后的数据
    std::cout << "ID: " << person.id() << std::endl;
    std::cout << "Name: " << person.name() << std::endl;
    std::cout << "Email: " << person.email() << std::endl;
}

这些代码展示了 Protobuf 在 C++ 中的实际使用:**将 Person 对象序列化到二进制文件中,并能反序列化回来,恢复成对象。**由于 Protobuf 的二进制格式非常紧凑,这个过程比 JSON 等文本格式更加高效。

4. 性能对比与优化分析

为了进一步证明 Protobuf 的性能优势,我们可以通过对比 Protobuf 与其他常见的序列化格式(如 JSON)的数据体积和解析速度,来展示其高效性。

4.1 数据大小对比

我们分别使用 Protobuf 和 JSON 来序列化相同的 Person 对象,结果如下:

  • Protobuf 二进制格式:大约 15 字节。
  • JSON 格式:约 60 字节。

这意味着 Protobuf 的二进制格式相比 JSON,节省了超过 75% 的数据存储空间。

4.2 解析速度对比

与 JSON 不同,Protobuf 使用预定义的 Schema 解析二进制数据,这避免了运行时的类型推断开销。以下是基于 C++ 的性能基准测试示例:

#include <chrono>
#include <nlohmann/json.hpp> // 需要安装 JSON 库

void SerializeJSON(const std::string& filename) {
    nlohmann::json j;
    j["id"] = 123;
    j["name"] = "Alice";
    j["email"] = "[email protected]";

    std::ofstream output(filename);
    output << j.dump();
}

void DeserializeJSON(const std::string& filename) {
    nlohmann::json j;
    std::ifstream input(filename);
    input >> j;

    std::cout << "ID: " << j["id"] << std::endl;
    std::cout << "Name: " << j["name"] << std::endl;
    std::cout << "Email: " << j["email"] << std::endl;
}

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    SerializePerson("person.bin");
    DeserializePerson("person.bin");
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> protobuf_duration = end - start;
    std::cout << "Protobuf Duration: " << protobuf_duration.count() << " seconds\n";

    start = std::chrono::high_resolution_clock::now();
    SerializeJSON("person.json");
    DeserializeJSON("person.json");
    end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> json_duration = end - start;
    std::cout << "JSON Duration: " << json_duration.count() << " seconds\n";

    return 0;
}

基于此,我们可以看到 Protobuf 在序列化和反序列化速度上明显优于 JSON。

5. 示意图说明

最后,我们用文字描述 Protobuf 的编码流程,以帮助大家更直观地理解其高效性。

5.1 编码流程
+----------------+         +---------------------+
|   Person 对象   |  ---->  | Protobuf 二进制编码 |
+----------------+         +---------------------+
        |                              |
        | 1. 按字段编号顺序编码           |
        | 2. 使用 Varint 编码整数类型      |
        | 3. 字符串类型使用长度前缀编码    |
        | 4. 生成紧凑的二进制数据          |
        |                              |
        V                              V
+----------------+         +---------------------+
| 序列化后的二进制 |         | 反序列化为 Person 对象 |
+----------------+         +---------------------+
  1. 序列化

    • 按照 .proto 文件中定义的字段编号顺序,将每个字段编码为二进制数据。
    • 整数类型使用 Varint 编码,字符串类型先编码长度,再编码实际字符串内容。
    • 最终生成紧凑的二进制数据,适合高效传输和存储。
  2. 反序列化

    • 读取二进制数据,按照字段编号和类型信息解析出各个字段。
    • 由于有预定义的 Schema,可以快速定位和解析字段,重建原始对象。
5.2 字段编码示例

以下是 Person 消息的 idnameemail 字段的二进制编码过程:

Field: id (field_number=1, type=Var

int)
Key: (1 << 3) | 0 = 0x08
Value: 123 -> Varint = 0x7B
Encoded: 0x08 0x7B

Field: name (field_number=2, type=Length-delimited)
Key: (2 << 3) | 2 = 0x12
Value: "Alice" -> Length=5 -> Varint=0x05, Data=0x41 0x6C 0x69 0x63 0x65
Encoded: 0x12 0x05 0x41 0x6C 0x69 0x63 0x65

Field: email (field_number=3, type=Length-delimited)
Key: (3 << 3) | 2 = 0x1A
Value: "[email protected]" -> Length=16 -> Varint=0x10, Data=... (16 bytes)
Encoded: 0x1A 0x10 ... (16 bytes)

总结

通过以上分析,我们可以得出 Protobuf 的高效性能源于以下几点:

  1. 紧凑的二进制编码:减少数据体积,提升传输和存储效率。
  2. 预定义 Schema:避免运行时的类型推断,提升解析速度。
  3. 高效的 Varint 编码:对小整数有特别好的压缩效果。
  4. 高度优化的 C++ 实现:充分利用语言特性进一步提高性能。

Protobuf 因此成为了在高性能系统中数据序列化的首选,尤其适用于大规模、高频率数据交换的场景。

参考

0voice · GitHub

标签:std,编码,Protobuf,二进制,解密,C++,Person,序列化
From: https://blog.csdn.net/weixin_43925427/article/details/142650539

相关文章

  • dev c++ cout中文显示不出来怎么办
    比如你随便创建了一个项目,起初“牛逼”这两个字应该是不显示出来的,但是当你的光标在这一行会时显示出来,等你编译运行时控制台也是乱码。点击工具(tool)选择第一个编译选项,填入:“-fexec-charset=GBK”并勾选选择第三个,编辑器选项取消这个勾选,点击确定就好了......
  • Qt/C++音视频开发 - Onvif时间设置
    Qt/C++音视频开发-Onvif时间设置介绍Onvif(OpenNetworkVideoInterfaceForum)是一种开放的网络视频接口标准,旨在实现不同品牌设备之间的互操作。为了确保网络摄像头和其他视频设备的时间同步,Onvif提供了时间设置功能,这对于准确记录事件和协调多个设备的活动至关重要。......
  • C++(关键字)
    5.作用域限定符::5.1名字空间(掌握)名字空间是一种代码的层级划分。#include<iostream>usingnamespacestd;//C++课程中几乎所有的类型(不包括基本数据类型)都在std中inta=1;//新建一个名字空间namespacemy_space{inta=3;strings="哈哈......
  • 加解密demo(java、php)
    数据格式*@paramargs*撞库---入参加密字段signs加密前格式**{*"mobileMask":"134123412**",*"city":"武汉",*"system":"qxh"*}**撞库---返回加密字......
  • C++在游戏开发中的卓越性能:优势解析与代码示例
    在游戏开发领域,C++一直是一种备受青睐的编程语言。它以其高性能、灵活性和强大的功能集,成为了游戏开发者的首选语言之一。在本文中,我们将深入探讨C++在游戏开发中的优势,并提供一些代码示例来展示这些优势是如何在实际开发中发挥作用的。高性能与低级控制C++提供接近硬件......
  • 踏上C++游戏开发之旅:初学者指南与实战代码
    游戏开发是一个充满挑战和创造力的领域,而C++作为其中的一种强大工具,为开发者提供了实现他们最狂野游戏创意的能力。如果你是一个初学者,想要开始学习C++游戏开发,那么这篇文章将为你提供一条清晰的学习路径和实用的代码示例,帮助你迈出第一步。1.基础知识:C++和计算机科学在......
  • C++ 语言特性04 - decltype关键字
    一:概述        decltype是C++11引入的一个关键字,用于查询表达式的类型,而不对表达式进行求值。它的主要作用是在编译时确定变量或表达式的类型,非常适合在模板编程和泛型编程中使用。二:使用场景    1.获取变量的类型intx=10;decltype(x)y=20;//......
  • C/C++算法编程笔记(2024.9.26-9.30)
    一、并查集学习一:1、寻找根节点(两种)intfind(intx){if(x!=city[x]) city[x]=find(city[x]);returncity[x];}intfind(intx){ returnfa[x]==x?x:fa[x]=find(fa[x]);}2、合并不同集合voidmerge(intx,inty){inta=find(x);intb......
  • 【C++】set详解
    ......
  • 南沙C++信奥赛陈老师解一本通题: 1963:【13NOIP普及组】小朋友的数字
    ​ 【题目描述】有 nn 个小朋友排成一列。每个小朋友手上都有一个数字,这个数字可正可负。规定每个小朋友的特征值等于排在他前面(包括他本人)的小朋友中连续若干个(最少有一个)小朋友手上的数字之和的最大值。作为这些小朋友的老师,你需要给每个小朋友一个分数,分数是这样规定的:......