我们在序列化、反序列化 Protobuf message 时为了最小化内存拷贝,可以实现其提供的 ZeroCopyStream(包括 ZeroCopyOutputStream 和 ZeroCopyInputStream)接口类,ZeroCopyStream 要求能够进行 buffer 的分配,这体现在一个名为 Next 的接口上,这样做的好处是避免进行内存的拷贝,为了方便理解,我们来看一下 ZeroCopyInputStream 和传统的 stream 的对比:
- // 典型的做法,我们调用 input stream 的 Read 从内存中读取数据到 buffer
- // 这里进行了一次拷贝操作,也就是拷贝内存中的数据到 buffer 中
- // 之后 DoSomething 才能处理此数据
- char buffer[BUFFER_SIZE];
- input->Read(buffer, BUFFER_SIZE);
- DoSomething(buffer, BUFFER_SIZE);
- // 使用 Next 接口的做法,input stream 内部有责任提供(分配)buffer
- // 也就是说,DoSomething 可以直接操作内部的内存,而无需拷贝后再操作
- // 这就避免了一次内存拷贝
- const void* buffer;
- int size;
- input->Next(&buffer, &size);
- DoSomething(buffer, size);
本文探讨一下 ZeroCopyOutputStream(ZeroCopyInputStream 类似,就不详细说明了)。ZeroCopyOutputStream 接口类有以下几个接口需要实现:
- // 获得(分配)一个用于写入数据的 Buffer 给调用者
- virtual bool Next(void ** data, int * size) = 0;
- // 由于调用 Next 请求 stream 分配了一块 Buffer 给调用者使用
- // 在最后一次 Next 调用时可能分配的 Buffer 大小大于需要的大小
- // BackUp 接口用于归还多余的内存
- virtual void BackUp(int count) = 0;
- // 返回总的被写入的字节数
- virtual int64 ByteCount() const = 0;
Next 接受两个参数 data 和 size,data 和 size 保证不为 NULL,data 用于获取 Buffer 的地址,size 用于获取 Buffer 的长度。此函数被调用意在获取一个可以用来写入数据的连续的 Buffer。函数返回 false 表示调用失败。
BackUp 的意义在于,调用了 Next 之后获取到一块大小为 size 的 Buffer 用于写入数据,但有可能出现数据全部写入完了 Buffer 还未使用完,这时候需要调用 BackUp 把未使用的内存归还。
https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.io.zero_copy_stream#ZeroCopyOutputStream 详细的描述了每个接口的前置和后置条件。在 Protobuf 中已经包含多个 ZeroCopyStream 的实现,例如:ArrayOutputStream,它们都可以作为我们实现 ZeroCopyStream 的范例。