最快的C++序列化方法?

24

我正在寻找一种非常快速的二进制序列化技术,用于C++。 我只需要序列化对象中包含的数据(没有指针等)。 我希望它尽可能快速。 如果它是特定于x86硬件的,那就可以接受。

我熟悉使用C进行此操作的方法。 作为测试,我对几种技术进行了基准测试。 我发现C方法比我实施的最佳C ++方法快40%。

有关如何改进C ++方法(或执行此操作的库)的建议吗? 有任何好的内存映射文件可用吗?

// c style writes
{
   #pragma pack(1)
   struct item
   {
      uint64_t off;
      uint32_t size;
   } data;
   #pragma pack

   clock_t start = clock();

   FILE* fd = fopen( "test.c.dat", "wb" );
   for ( long i = 0; i < tests; i++ )
   {
      data.off = i;
      data.size = i & 0xFFFF;
      fwrite( (char*) &data, sizeof(data), 1, fd );
   }
   fclose( fd );

   clock_t stop = clock();

   double d = ((double)(stop-start))/ CLOCKS_PER_SEC;
   printf( "%8.3f seconds\n", d );
}

对于测试 = 10000000,大约需要1.6秒

// c++ style ofstream writes

// define a DTO class
class test
{
public:
   test(){}

   uint64_t off;
   uint32_t size;

   friend std::ostream& operator<<( std::ostream& stream, const test& v );
};

// write to the stream
std::ostream& operator<<( std::ostream &stream,  const test& v )
{
   stream.write( (char*)&v.off, sizeof(v.off) );
   stream.write( (char*)&v.size, sizeof(v.size) );
   return stream;
}

{
   test data;

   clock_t start = clock();

   std::ofstream out;
   out.open( "test.cpp.dat", std::ios::out | std::ios::trunc | std::ios::binary );
   for ( long i = 0; i < tests; i++ )
   {
      data.off = i;
      data.size = i & 0xFFFF;
      out << data;
   }
   out.close();

   clock_t stop = clock();

   double d = ((double)(stop-start))/ CLOCKS_PER_SEC;
   printf( "%8.3f seconds\n", d );
}

在测试数量为10000000时,大约需要2.6秒。


13
这不是序列化,而是内存转储。如果您的对象的内存布局发生了改变,或者从大端平台切换到小端平台,它将无法正常工作。 - Matthieu M.
1
这不是相等的代码。在operator<<中,您应该只是<< v.off/v.size,而不是您所做的。您还没有打包测试类,也没有在C中截断文件,在C++中您进行了两次写入调用,每个成员都有一次,而在C中您一次性写入整个结构。 - Puppy
2
@Matthieu:(来自维基百科)序列化:“序列化是将数据结构或对象转换为一系列位,以便可以存储在文件或内存缓冲区中的过程。”根据这个定义,似乎符合要求。我不需要担心不同架构之间的互操作性。 - Jay
1
@Dead: 你不需要打包类,因为成员是分别写的。在 C++ 中,我能否像使用 C 结构体一样使用类? - Jay
1
@Paul:“C++流非常慢。” - 但在这种情况下不是。在这种情况下,C++实现明显不如C变体,但应该与C变体一样好。这也可能是由于iostreamstdio中不同的默认缓冲设置。 - Dummy00001
显示剩余3条评论
11个回答

13

实际情况下,只有极少数情况需要考虑这个问题。你只需要序列化来使对象与某种外部资源兼容,如磁盘、网络等等。传输序列化数据的代码总是比序列化对象所需的代码慢得多。如果你让序列化代码快两倍,整个操作只会变快0.5%左右。这既不值得风险,也不值得努力。

三思而后行。


直到你有大量的结构体数组,这时序列化性能将成为瓶颈。传输二进制数据通常只需要一个系统调用和memcpy(可以一次传输多个对象)。 - Hugo Maxwell

7
如果要执行的任务确实是序列化,您可以查看谷歌的Protocol Buffers。它们提供了C++类的快速序列化。该网站还提到了一些替代库,例如boost.serialization(仅说明协议缓冲区在大多数情况下优于它们,当然;-))。

2
Protocol Buffers(尽管我很喜欢它)并不是真正的序列化,而更适用于消息传递。区别在于,对于协议缓冲区,您定义了一个消息类,而在序列化中没有中间表示。 - Matthieu M.
1
再仔细考虑一下,您可以使用protobuf类在实际类中保存数据,这样您就可以在隐藏此事实的同时使用protobuf进行数据保留和编码/解码。 - Matthieu M.
我写了一个类似 Protobuf 的 header-only 库:https://github.com/earonesty/qserial。它的速度与 proto 差不多,但更容易在奇怪的平台上使用。 - Erik Aronesty

3

3

他们的基准测试显示,相比于原始二进制读取,它慢了四倍。我可以看出可能有一些情况下这会更好。如果你正在处理大量数据,而你不需要使用其中的全部,那么总体上可能会更快。 - Jay

2

如果您想要最快的序列化,那么您可以编写自己的序列化类,并为每个POD类型提供序列化方法。

带来的安全性越少,它运行得就越快,但是调试起来就会更加困难,不过内置的类型数量是固定的,因此您可以枚举它们。

class Buffer
{
public:
  inline Buffer& operator<<(int i); // etc...
private:
  std::deque<unsigned char> mData;
};

我必须承认我不理解你的问题:

  • 你实际上想用序列化消息做什么?
  • 你是要将其保存以便以后使用吗?
  • 你需要担心前向/后向兼容性吗?

也许有比序列化更好的方法。


我将会将数据持久化到磁盘上。它只能在保存数据的同一台机器上加载。我正在考虑给对象添加版本号,这样能更好地处理变更。如果你知道更好的方法,我很乐意听取建议。 - Jay
版本控制是必须的,否则你就会陷入困境。你可以根据需要进行版本控制,因为对每个结构体进行版本控制会使事情变得更加昂贵,而只有一个版本也不容易维护。我还建议在各个地方使用一些“同步”标记,以及可能使用CRC代码来检查数据完整性(以防文件损坏)。我已经在Thorsten77的答案中评论了protobuf,它看起来可以帮助你很多。 - Matthieu M.

1

有没有办法利用那些保持不变的事物呢?

我的意思是,你只是尽可能快地试图运行“test.c.dat”,对吗?你能利用文件在序列化尝试之间不改变的事实吗?如果你正在尝试一遍又一遍地序列化同一个文件,你可以根据此进行优化。我可以让第一次序列化尝试花费与你相同的时间,再加上一点额外的检查时间,然后如果你试图在相同的输入上再次运行序列化,我的第二次运行会比第一次运行快得多。

我知道这可能只是一个精心制作的例子,但你似乎专注于让语言尽快完成任务,而不是问“我需要再次完成这个任务吗?”这种方法的背景是什么?

希望这对你有所帮助。

-Brian J. Stinar-


它将被用作配置数据库。我编写的代码只是为了测试方法的开销。不过这是个好主意。 - Jay

1

那是我接下来要做的事情。感谢确认。 - Jay

1

很多性能取决于内存缓冲区以及在写入磁盘之前如何填充内存块。而且有一些技巧可以使标准的C++流更快,比如std::ios_base::sync_with_stdio(false);

但是在我看来,世界上不需要另一个序列化实现。以下是其他人维护的一些你可能想要了解的序列化实现:

  • Boost:快速、各种各样的C++库,包括序列化
  • protobuf:带有C++模块的快速跨平台、跨语言序列化
  • thrift:带有C++模块的灵活跨平台、跨语言序列化

5
如果你能展示一个在受限环境下具有确定性内存使用的序列化包,我会向你展示唯一值得使用的序列化包。在此之前,如果每个人对序列化有着相互矛盾的需求,那么说我们不需要另一个序列化包是有些牵强附会的。 - MSN
我看了一下boost。它为序列化任何对象而跳过各种障碍,而我只需要POD。为什么要支付你不需要的额外费用呢? - Jay
1
@Jay:如果你只需要POD的支持,为什么不直接使用你的C方法呢? - jalf
我希望这里有人想到了我没有想到的东西 :( - Jay

1
因为输入/输出很可能成为瓶颈,所以采用紧凑的格式可能会有帮助。出于好奇,我尝试了以下编译为 colf -s 16 C 的 Colfer 方案。
    package data

    type item struct {
            off  uint64
            size uint32
    }

...与一个相似的C测试:
    clock_t start = clock();

    data_item data;
    void* buf = malloc(colfer_size_max);

    FILE* fd = fopen( "test.colfer.dat", "wb" );
    for ( long i = 0; i < tests; i++ )
    {
       data.off = i;
       data.size = i & 0xFFFF;
       size_t n = data_item_marshal( &data, buf );
       fwrite( buf, n, 1, fd );
    }
    fclose( fd );

    clock_t stop = clock();

尽管串行大小比原始结构转储小40%,但SSD的结果令人失望。

    colfer took   0.520 seconds
    plain took    0.320 seconds

由于生成的代码非常快速, 使用序列化库似乎不太可能获得任何优势。


0

你的 C 和 C++ 代码中,文件 I/O 可能会占据主导地位(在时间上)。我建议在写入数据时使用内存映射文件,并将 I/O 缓冲留给操作系统。Boost.Interprocess 可以作为一种替代方案。


内存映射文件非常特定于操作系统。 - Jay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接