最便捷、最强大、速度最快的C++序列化框架
迄今为止,我还没找到更优雅、更高效的 C++ 序列化方案,包括但不限于 boost.serialization。如果你发现了更快的,或者更易用的C++原生序列化,请告诉我……
我这个序列化库的特点
- 高性能,速度非常快,比你能找到的同类产品至少快一个数量级
- 用在网络通讯,数据库存储中恰到好处
- 预先支持所有基本类型,所有stl容器类型(除stack/queue之外)
- 支持变长 int32/uint32/int64/uint64
- 支持 stl::pair, boost::tuple
- 可选的版本控制,而非强制
- 对于小对象,通常不需要版本控制
- boost::serialization的版本号是强制的,当初我设计这个序列化框架就是因为boost不能省略版本号
- 非侵入式设计,不污染名字空间
- 声明式语法,简单,可靠
- 等待你发现更多……
- 特别注意:vc6太古老,不符合C++规范,无法使用该框架
该框架的易用性
还是用代码说明问题 (Talk is cheap. Show me the code. Torvalds, Linus (2000-08-25) ),先看这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
struct MyData1 { int a, b, c; var_int32_t d; // d.t is int32 value var_uint64_t e; // e.t is uint64 value std::string f; std::map<std::string, int> g; std::set<int> h; // 声明序列化,无版本控制,最简洁的声明,后面几个稍微复杂点 DATA_IO_LOAD_SAVE(MyData1, &a&b&c&d&e&f&g&h) }; struct MyData2 { int a, b, c; var_int32_t d; var_uint64_t e; std::string f; std::map<std::string, int> g; std::set<int> h; // 声明序列化,有版本控制 DATA_IO_LOAD_SAVE_V(MyData2, 1, // 当前版本 &a&b&c&d&e&f&g&h ) }; struct MyData3 { int a, b, c; boost::int32_t d; boost::uint64_t e; std::string f; std::map<std::string, int> g; std::set<int> h; std::multiset<int> i; unsigned version; // 声明序列化,有版本控制 DATA_IO_LOAD_SAVE_V(MyData3, 2, // 当前版本 &a &b &c &as_var_int(d) // d 声明为int32_t, 但是作为var_int32_t 来存储 &as_var_int(e) // e 声明为uint64_t, 但是作为var_uint64_t 来存储 &f &g &h &vmg.since(2, i) // 版本2 新增了成员i &vmg.get_version(version) // 如果需要,将版本值存入version 成员 ) }; int main(int argc, char* argv[]) { PortableDataOutput<AutoGrownMemIO> output; PortableDataInput<MemIO> input; output.resize(1024); // 可选,没有这一行就需要扩张几次,相当于 vector.reserve MyData1 d1; // set d1 values // ... MyData2 d2; // set d2 values // ... MyData3 d3; // set d3 values // ... output << d1 << d2 << d3; // 存储 input = output.head(); // 浅拷贝,将 input 设为 output 已写入的那部分 input >> d1 >> d2 >> d3; // 载入 //---------------------------------- // operator& 与operator<< 等效 output & d1 & d2 & d3; // 存储 input = output.head(); // 浅拷贝,将 input 设为 output 已写入的那部分 // operator& 与operator>> 等效 input & d1 & d2 & d3; // 载入 } |
模仿这段代码,可以完成大部分的现实需求,如果有更多的需求,可以使用该框架的高级功能。例如,系统中已经定义了一些数据结构,但又不能修改现有代码,怎样给它们增加序列化能力呢?请看如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// in system header, can not change struct SysData1 { int a; unsigned b; string c; }; // add these 2 function in your header template<class DataIO> void DataIO_saveObject(DataIO& dio, const SysData1& x) { dio & x.a & x.b & x.c; } template<class DataIO> void DataIO_loadObject(DataIO& dio, SysData1& x) { dio & x.a & x.b & x.c; } // DataIO 新版中,更简单的方法 DATA_IO_LOAD_SAVE_E(SysData2, &a &b &c) // ####################################################################### // 如果现存的对象需要版本控制,参考如下代码: struct SysData2 { int a; unsigned b; string c; }; // add these 2 function in your header template<class DataIO> void DataIO_saveObject(DataIO& dio, const SysData2& x) { const unsigned curr_version = 2; dio & serialize_version_t(curr_version); dio & x.a & x.b; dio & x.c; } template<class DataIO> void DataIO_loadObject(DataIO& dio, SysData2& x) { const unsigned curr_version = 2; serialize_version_t loaded_version; in >> loaded_version; if (loaded_version.t > curr_version) { throw BadVersionException(loaded_version.t, curr_version, className); } dio & x.a & x.b; if (loaded_version.t >= 2) dio & x.c; } // DataIO 新版中,更简单的方法: DATA_IO_LOAD_SAVE_EV(SysData2, &a &b& vmg.since(2, c)) |
How It Works
DataIO_loadObject/DataIO_saveObject只要在调用点可见,就可以对 SysData 进行序列化。因为 DataIO 序列化框架使用DataIO_loadObject/DataIO_saveObject来载入和存储对象,这样做的好处有以下几点:
l 非侵入,对象类型和加载/存储函数可以分离定义
n 否则无法为不可更改代码的对象增加序列化能力
l 这两个函数可以定义在任何名字空间
n 根据C++的名字查找规则,只要在调用环境和每个参数所在的名字空间中有相应的匹配函数,就会使用该函数。我们需要有效地利用这一点。
l DataIO_loadObject/DataIO_saveObject这两个函数名较长,并且罕见
n 因此不会与系统中的其他标识符发生冲突。(对比boost::serialization中的serialize函数,它就比较容易和其他名字发生冲突,serialize太常见了)。
性能
在上面的代码中可以看到几个陌生的名字:MemIO, AutoGrownMemIO,PortableDataOutput, PortableDataInput…
但上面的示例代码中没有用到MinMemIO,因为 MinMemIO 没有越界检查,只有在非常简单,完全可控的情况下,才能使用它。因为没有越界检查,它的性能非常好,在大多数情况下相当于手写的 memcpy 序列化。
使用 MemIO 会稍微慢一点,但是有越界检查,读取时越界会抛出 EndOfFileException 异常,写入越界时会抛出 OutOfSpaceException 异常。
使用AutoGrownMemIO,在save时,碰到边界会自动增加内存(相当于vector.push_back自动增加内存),也可以使用resize预先分配内存(相当于vector.reserve/resize)。
这个几个MemIO类都非常简单,速度快是很自然的。
在PortableDataOutput,PortableDataInput中,如果机器字节序是LittleEndian,需要交换字节序,这一点,在新版的vc中和gcc中,会直接映射到一条指令:bswap。所以也不会有性能问题。
对于var_int的存储,无符号数,每个字节包含7个有效位,余下一位表示是否需要读取下一个字节。因此0~127仅需要一个字节,0~2^14-1需要两个字节,等等。对于有符号数,最低有效位存储符号,其余位存储绝对值。所有stl容器和string的尺寸就是用var_uint32_t存储的。
该框架中,同时实现了StreamBuffer,可以为任意Stream增加一层缓冲,往缓冲里面序列化数据的效率和MemIO系列是一样的,不同之处在于当缓冲耗尽或填满时会调用真实Stream的读写方法。这比起通常很多实现中将BufferedStream作为一个抽象,在读取哪怕一个字节时也需要一个虚函数调用,速度要快得多。
扩展应用
使用该序列化框架,我实现了一个不需要 IDL的 RPC 。
使用该序列化框架,对 Berkeley DB 进行包装,可以让它象标准容器一样使用,免除了复杂的编码。后面我会继续介绍。
非常赞,可以整理一下申请加入Boost库了!
加油!
要加入boost,文档、代码结构、代码风格等等都要符合boost,工程浩繁哪
请教一下,boost的序列化支持直接序列化图,树等结构, 会自动合并相同的指针引用. 这个库是怎么做到这一点的呢?
这个实现方法很多,最简单直观的:存储时,在 Archive 中加个 map<ptr,int> ,对于 ptr ,先查 map, 如果没找到,给它分配一个整数,接着写数据,如果找到了,直接写入 ptr 对应的那个整数。反序列化/加载 时刚好相反,是 map<int,ptr> ……
强大,看来楼主对泛型编程技术已经炉火纯青了!确实钻研STL很久了。但标准库的要求和考量是十分苛刻的啊
博主确实很强,但(code&document)可读性不强,可以看看陈硕先生,document很全很好读
貌似很强大的样子
楼主的几篇博文确实让我大开眼界。就这篇来看,请问有考虑指针的深度拷贝和多指针指向同一对象时共享一份拷贝时设计吗?从文中看不出来
这个不支持,因为它需要保持额外的状态,你可以看我前面的回复
[reply]lonelyrains[/reply]
我也就是路人甲,询问下,你这个支持跨平台吗?? 就是不同机器(大小端)的数据传输??
支持,示例中的 PortableDataInput 和 PortableDataOutput 就是用来实现跨平台序列化的( 不管机器是什么 Endian,都是按 Big Endian 读写)