如何减少大量Protocol Buffer消息实例的内存分配次数?
Protocol Buffer 批量消息场景的内存分配优化
问题背景
处理1亿条简单Protocol Buffer消息时,通过Callgrind分析发现每个Nested实例都会触发一次独立的内存分配与释放,导致内存操作开销随实例数量线性增长。示例代码如下:
.proto定义
syntax = "proto2"; package testpb; message Top { message Nested { optional int32 val1 = 1; optional int32 val2 = 2; optional int32 val3 = 3; } repeated Nested data = 1; }
原C++实现
void test() { testpb::Top top; for (int i = 0; i < 100'000; ++i) { auto* data = top.add_data(); data->set_val1(i); data->set_val2(i*2); data->set_val3(i*3); } std::ofstream ofs{"file.out", std::ios::out | std::ios::trunc | std::ios::binary }; top.SerializeToOstream(&ofs); }
优化方案
1. 使用Arena内存分配器(推荐)
Protocol Buffer 3.10及以上版本支持Arena内存分配器,它会将所有对象分配在连续的内存块中,避免每个对象单独调用malloc/free,内存分配次数从与实例数量线性相关变为与内存块数量相关。
修改后的代码:
#include <google/protobuf/arena.h> void test() { google::protobuf::Arena arena; // 在Arena上创建Top对象 testpb::Top* top = google::protobuf::Arena::CreateMessage<testpb::Top>(&arena); // 预分配repeated字段的空间,避免扩容时的额外分配 top->mutable_data()->Reserve(100'000); for (int i = 0; i < 100'000; ++i) { // 在Arena上创建Nested对象,无需手动释放 testpb::Top::Nested* data = google::protobuf::Arena::CreateMessage<testpb::Top::Nested>(&arena); data->set_val1(i); data->set_val2(i*2); data->set_val3(i*3); // 使用AddAllocated添加对象,避免拷贝开销 top->mutable_data()->AddAllocated(data); } std::ofstream ofs{"file.out", std::ios::out | std::ios::trunc | std::ios::binary }; top->SerializeToOstream(&ofs); // Arena会在析构时自动释放所有分配的内存,无需手动清理 }
2. 手动对象池复用(适用于旧版本Protobuf)
如果无法使用Arena,可以手动维护一个Nested对象池,重复利用已分配的内存,减少分配次数:
#include <vector> void test() { testpb::Top top; top.mutable_data()->Reserve(100'000); std::vector<testpb::Top::Nested*> object_pool; for (int i = 0; i < 100'000; ++i) { testpb::Top::Nested* data = nullptr; if (!object_pool.empty()) { // 从池中取对象复用 data = object_pool.back(); object_pool.pop_back(); data->Clear(); // 重置对象状态 } else { // 池为空时新建对象 data = new testpb::Top::Nested(); } data->set_val1(i); data->set_val2(i*2); data->set_val3(i*3); top.mutable_data()->AddAllocated(data); } std::ofstream ofs{"file.out", std::ios::out | std::ios::trunc | std::ios::binary }; top.SerializeToOstream(&ofs); // 回收对象到池,供后续复用 for (auto* data : top.data()) { top.mutable_data()->Release(data); // 从repeated字段中释放所有权 object_pool.push_back(data); } // 最终销毁池中的对象(按需) for (auto* data : object_pool) { delete data; } }
3. 预分配repeated字段空间
无论使用哪种方案,都应该调用Reserve()方法预分配repeated字段的容量,避免字段在扩容时多次内存分配,进一步降低分配次数。
内容的提问来源于stack exchange,提问作者MarkB
相关产品推荐
相关产品推荐

