找回密码
 立即注册
查看: 590|回复: 0

C++ Arena相关API解析

[复制链接]
发表于 2021-11-23 16:06 | 显示全部楼层 |阅读模式
这篇是protobuf 提供的c++ API (部分Arena源码)学习记录。
protobuf c++ API 分四个部分: ① google::protobuf ②google::protobuf::io ③google::protobuf::util ④google::protobuf::compiler
google::protobuf

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

  • Arena Allocator
Arena解决的问题:默认情况下,proto为对象,为其子对象,或者其他的一些类型(string bytes)都会在堆上申请内存(这里我还没有去看message的构造机制,Todo),因此构造新消息或者解析消息会出现多次堆内存申请,析构时有多次堆内存释放。
使用Arena后,有效避免上述问题,对象的内存是从Arena预申请的一片大内存空间中获取,在Arena释放时对象被统一释放。将频繁的申请、释放内存的行为改变成指针偏移, 效率提高。另外缓存效率更高,原因是在解析消息时,从Arena获取连续的内存,遍历消息时更有机会命中缓存。【参考官方文档】
Arena Allocator for better allocation performance.(Arena的分配性能更佳)
相关实现文件:arena.h http://arean.cc arena_impl.h 相关测试文件:arena_test_util.h http://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 源码
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-25 14:37 , Processed in 0.090189 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表