You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何减少大量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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 23:45:41