Boost 和 Cereal没有实现 Cap'n Proto 或 Flatbuffers 意义上的零拷贝。
read()
使用真正的零拷贝序列化,实时内存对象的后备存储实际上与传递给orwrite()
系统调用的内存段完全相同。根本没有打包/拆包步骤。
一般来说,这有很多含义:
- 不使用 new/delete 分配对象。构造消息时,首先分配消息,它为消息内容分配一个长的连续内存空间。然后,您直接在 message 内部分配消息结构,接收实际上指向消息内存的指针。稍后写入消息时,一次
write()
调用会将整个内存空间推到网络上。
- 同样,当您读入一条消息时,一次
read()
调用(或者可能是 2-3 次)会将整条消息读入一个内存块。然后,您将获得一个指向消息“根”的指针(或类似指针的对象),您可以使用它来遍历它。请注意,在您的应用程序遍历它之前,实际上不会检查消息的任何部分。
- 使用普通套接字,数据的唯一副本发生在内核空间中。使用 RDMA 网络,您甚至可以避免内核空间复制:数据从线路直接进入其最终内存位置。
- 在处理文件(而不是网络)时,可以
mmap()
直接从磁盘获取非常大的消息并直接使用映射的内存区域。这样做是 O(1) - 文件有多大并不重要。当您实际访问文件的必要部分时,您的操作系统将自动分页。
- 同一台机器上的两个进程可以通过没有副本的共享内存段进行通信。请注意,一般来说,常规的旧 C++ 对象在共享内存中不能很好地工作,因为内存段在两个内存空间中通常没有相同的地址,因此所有指针都是错误的。对于零拷贝序列化框架,指针通常表示为偏移量而不是绝对地址,因此它们与位置无关。
Boost 和 Cereal 不同:当您在这些系统中收到一条消息时,首先会对整个消息执行一次传递以“解包”内容。数据的最终存放位置是使用 new/delete 以传统方式分配的对象中。类似地,当发送消息时,必须从这棵对象树中收集数据并将其打包到一个缓冲区中才能被写出。尽管 Boost 和 Cereal 是“可扩展的”,但真正实现零拷贝需要非常不同的底层设计;它不能作为扩展用螺栓固定。
也就是说,不要假设零拷贝总是更快。memcpy()
可以相当快,而您的程序的其余部分可能会使成本相形见绌。同时,零拷贝系统往往有不方便的 API,特别是因为内存分配的限制。总体而言,使用传统的序列化系统可能会更好地利用您的时间。
零拷贝最明显有利的地方是在处理文件时,因为正如我所提到的,您可以轻松地mmap()
获得一个巨大的文件并且只读取其中的一部分。非零拷贝格式根本无法做到这一点。但是,在网络方面,优势不太明显,因为网络通信本身必然是 O(n)。
归根结底,如果您真的想知道哪种序列化系统最适合您的用例,您可能需要全部尝试并测量它们。请注意,玩具基准通常具有误导性;您需要测试您的实际用例(或非常类似的东西)以获得有用的信息。
披露:我是 Cap'n Proto(零拷贝序列化程序)和 Protocol Buffers v2(流行的非零拷贝序列化程序)的作者。