ProtoBuf Summary(二)C++ Arena API & 源码

这篇是protobuf 提供的c++ API (部分Arena源码)学习记录。
protobuf c++ API 分四个部分: ① google::protobuf ②google::protobuf::io ③google::protobuf::util ④google::protobuf::compiler

google::protobuf

说明:这部分是protocol buffer 运行时库的核心组件。另外,官方文档说除了特殊声明,只要对象在所有线程中都是const的,使用起来是线程安全的;如果不允许以const方式访问某个对象,则是线程不安全的。

  1. Arena Allocator

Arena解决的问题:默认情况下,proto为对象,为其子对象,或者其他的一些类型(string bytes)都会在堆上申请内存(这里我还没有去看message的构造机制,Todo),因此构造新消息或者解析消息会出现多次堆内存申请,析构时有多次堆内存释放。
使用Arena后,有效避免上述问题,对象的内存是从Arena预申请的一片大内存空间中获取,在Arena释放时对象被统一释放。将频繁的申请、释放内存的行为改变成指针偏移, 效率提高。另外缓存效率更高,原因是在解析消息时,从Arena获取连续的内存,遍历消息时更有机会命中缓存。【参考官方文档】

Arena Allocator for better allocation performance.(Arena的分配性能更佳)

相关实现文件:arena.h arean.cc arena_impl.h 相关测试文件:arena_test_util.h arena_unittest.cc
arena.h 中的类:

  • ArenaOptions
    内存块申请选项,比如Arena初始申请内存大小、最大内存限制,初始化使用的内存块,内存块申请函数、释放函数

  • Arena
    Allocator

  • Arena::InternalHelper
    Arena的内部类, 通过模板元编程的方式判断类型是否满足Arena构造条件

  • Arena::is_arena_constructable
    类型支持Arena

  • Arena::is_destructor_skippable
    类型支持不执行析构函数,并且是安全的

Arena API:

  • Arena()
    无参构造函数,创建Arena

  • Arena(const ArenaOptions& options)
    使用上述的特定选项创建Arena, 可以实现在向系统申请内存前,使用用户提供的内存,进行内存申请限制,自定义申请、释放函数

  • template static T* CreateMessage(Arena * arena)
    在给定的arena上创建proto对象,proto文件必须开启Arena;arena 非空,则返回在arena上创建的对象,注意这个对象是不能被手动释放的;如果arena是空,对象将和不用Arena时一样在堆上创建

  • template static T* Create(Arena* arena, args…)
    可以创建任何对象,不仅仅是proto对象,包括C++自定义类

  • template static T* CreateArray(Arena* arena, size_t n)
    arena非空情况下从Arena上申请N个对象空间大小,并且T的构造函数不会被调用,否则在堆上如普通创建

  • template void Own(T* object)
    将对象添加到arena的堆对象列表中。arena释放时,会遍历这个列表对每个对象进行delete。函数适用于不在arena上创建的对象,但是该对象的生存期又需要绑定到arena上的情况,(比如父message支持arena,submessage不支持arena在另一个proto文件中)

  • template void OwnDestructor(T* object)
    将对象的析构器放入arena的destructor列表中,(列表与Own列表应该不同) ,arena释放时,遍历列表调用保存的destructor。函数适用于对象使用的是Arena内存,但是析构函数不会被调用的情况,比如proto对象

  • uint64 SpaceUsed() const
    返回arena的大小

  • uint64 Reset()
    销毁arena,调用所有注册的析构函数,释放所有注册的堆对象(那两个Own列表),与arena的销毁过程类似,只是会reset内存,重新使用。这个函数非线程安全的

  • template Arena* GetArena()
    返回指向arena的指针

使用Arena, 需要设置option cc_enable_arenas = true; 下面说明启用Arena后,接口实现发生的变化。
对于Message提供接口发生一些变化:

  • Message(Message&& other)
    如果源消息不在arena上,移动构造函数会移动other所有的字段,无需拷贝和申请堆内存。如果源消息在arena上,则会进行深拷贝

  • Message& operator=(Message&& other)
    如果两个消息都不在arena上,将会进行移动构造。如果仅有其中之一在arena上,或者两个消息不在同一arena上,将会进行深拷贝

  • void Swap(Message* other)
    都没有使用arena或者都在同一arena上,通过交换指针避免拷贝开销。如果一个消息在arena上,而另一个不在,或者两个消息在不同的arena上,进行深拷贝。否则交换的子对象可能会有不同的生命周期(可以想象一下仅交换头指针的情况)

  • Message* New(Arena* arena)
    在给定的arena上创建对象。如果开启了cc_enable_arenas,函数与CreateMessage表现一致;否则相当于在普通内存申请,然后调用arena->Own(message)

  • Arena* GetArena()
    返回message所在arena指针

  • void UnsafeArenaSwap(Message* other)
    不对message是否在同一arena上做检查,比较高效。使用这个函数的时候要确保两个message在同一arena上,否则出现不可预知的结果

对于嵌套Filed接口发生的变化,例如
optional Bar foo = 1;
required Bar foo = 1;

  • Bar* mutable_foo()
    返回mutable指针,如果父对象在arena上,则子对象也在arena上

  • void set_allocated_foo(Bar* bar)
    将字段置为新对象的值。如果父对象在arena上,子对象在堆上,将子对象arena->own放入堆对象列表;如果两者不在同一arena上,会将message的拷贝置为新值;如果父子对象在同一arena上或者都在堆上,函数与未使用arena相同

  • Bar* release_foo()
    返回存在的子对象或者空指针,将归属权释放给调用者,清除父对象的该字段。如果父对象在arena上,将在堆上拷贝子对象,返回这个拷贝

  • void unsafe_arena_set_allocated_foo(Bar* bar)
    假设父子对象都在同一arena上,进行allocated

  • Bar* unsafe_arena_release_foo()
    假设父对象在arena上,返回在arena上的子对象,该函数只适用于父对象在arena的情况

对于String Field
即使父对象在arena上,string仍然在堆上申请内存。

对于Repeated Field

  • void UnsafeArenaSwap(RepeatedPtrField* other)
    unsafe版本的swap不会验证将要进行交换的RepeatedPtrField,是否拥有指向相同arena的指针

  • void Swap(RepeatedPtrField* other)
    进行arena指针检查,没有指向同一arena,则进行复制,然后swap

  • void AddAllocated(SubMessageType* value)
    subMessage和父message如果在同一arena上,直接sub的指针放入array;如果sub在堆上,释放原始的sub,将拷贝的副本放入array

  • SubMessageType* ReleaseLast()
    返回repeated字段最后一个对象,如果repeated字段在堆上,没有使用arena,直接返回原始的sub;否则在堆上进行拷贝,然后返回

  • void UnsafeArenaAddAllocated(SubMessageType* value)
    不进行堆或者arena的检查,直接放入array,调用者需要保证(如果repeated Field在arena上,那么value需在同一arena上或者有相同的生命周期,或者该field在堆上)

  • void ExtractSubrange(int start, int num, SubMessageType** elements)
    移除num个元素,如果元素在arena上,返回前会先拷贝到堆上

  • void UnsafeArenaExtractSubrange(int start, int num, SubMessageType** elements)
    移除num个元素,但是从不进行拷贝

官方文档例子说明:

message MyFeatureMessage {
  optional string feature_name = 1;
  repeated int32 feature_data = 2;
  optional NestedMessage nested_message = 3;
};

Arena* arena = new google::protobuf::Arena();
MyFeatureMessage* arena_message_1 = google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);
arena_message_1->mutable_nested_message()->set_feature_id(11);

MyFeatureMessage* arena_message_2 = google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);

arena_message_2->set_allocated_nested_message(arena_message_1->release_nested_message());
arena_message_1->release_message();//returns a copy of the underlying nested_message and deletes underlying pointer

使用Arena后,有可能导致无意识的拷贝,可以参考上面的API说明,unsafe的函数是为了避免拷贝的。

arena_message_2->set_allocated_nested_message(arena_message_1->unsafe_arena_release_nested_message());

swap低效用法:

MyFeatureMessage* message_1 = google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);
message_1->mutable_nested_message()->set_feature_id(11);

MyFeatureMessage* message_2 = new MyFeatureMessage;
message_2->mutable_nested_message()->set_feature_id(22);

message_1->Swap(message_2); // Inefficient swap!

swap高效用法,在相同的arena上创建对象,避免拷贝:

MyFeatureMessage* message_2 = google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);

源码一些细节:

参考资料:
https://developers.google.cn/protocol-buffers/docs/reference/arenas 官方文档

https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/arena.h 源码

protobuf中,可以使用不同语言编写的代码进行相互传递。对于Python和C++之间的相互传递,可以按照以下步骤进行操作: 1. 首先,我们需要使用protobuf协议来定义消息格式。可以使用.proto文件来定义消息结构。 2. 使用protobuf编译器(protoc)将.proto文件编译为对应语言的代码文件。对于Python,可以使用protobuf的python版本来编译。 可以按照中的方法,在protobuf-2.5.0\python目录下运行以下命令编译proto文件: ``` protoc -I=<proto文件所在目录> --python_out=<输出目录> <proto文件名>.proto ``` 这将生成一个.py文件,其中包含生成的Python代码。 3. 对于Python中的编译好的代码文件,可以直接在Python代码中导入并使用。 在Python代码中,需要使用`import`语句导入生成的.py文件,然后使用生成的类来创建和操作消息。 4. 对于C++中的编译好的代码文件,可以按照中的方法进行编译。 首先,确保已经安装了protobuf库。然后,在命令行中使用g++编译器将.proto文件和生成的代码文件进行编译。 ``` g++ -o <输出文件名> <proto文件名>.pb.cc <生成的代码文件名>.cpp -lprotobuf ``` 这将生成一个可执行文件,可以在C++中使用。 5. 在Python和C++之间进行相互传递时,可以使用protobuf提供的序列化和反序列化方法。在Python中,可以使用protobuf生成的类的`SerializeToString()`方法将消息序列化为字符串;在C++中,可以使用protobuf提供的方法将消息序列化为字节流。 在Python中,可以使用protobuf生成的类的`ParseFromString()`方法将接收到的字节流反序列化为消息对象;在C++中,可以使用protobuf提供的方法将接收到的字节流反序列化为消息对象。 这样,就实现了Python和C++之间的protobuf消息的互传。 需要注意的是,在使用protobuf过程中,大小写转换问题可能会导致一些错误。如所提到的,默认情况下,protoc会将变量的大小写转换为小写字母。在Python中,变量是区分大小写的。因此,在处理大小写问题时需要注意避免出错。 综上所述,即可实现Python和C++之间的protobuf消息互传。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值