C++ 竞技场分配指南
此页面准确描述了 Protocol Buffer 编译器在启用竞技场分配时除了在C++ 生成代码指南中描述的代码之外,还生成的 C++ 代码。它假设您熟悉语言指南和C++ 生成代码指南中的内容。
为什么要使用竞技场分配?
内存分配和释放占用了 Protocol Buffers 代码中相当一部分的 CPU 时间。默认情况下,Protocol Buffers 为每个消息对象、其每个子对象以及一些字段类型(如字符串)执行堆分配。这些分配在解析消息和在内存中构建新消息时批量发生,并且关联的释放发生在消息及其子对象树被释放时。
基于竞技场的分配旨在降低此性能成本。使用竞技场分配时,新对象将从称为竞技场的大块预分配内存中分配。所有对象都可以通过丢弃整个竞技场来一次性释放,理想情况下无需运行任何包含对象的析构函数(尽管在需要时竞技场仍可以维护“析构函数列表”)。这使得对象分配更快,因为它简化为简单的指针递增,并且使释放几乎免费。竞技场分配还提供了更高的缓存效率:当解析消息时,它们更有可能在连续内存中分配,这使得遍历消息更有可能命中热门缓存行。
要获得这些好处,您需要了解对象生命周期,并找到使用竞技场的合适粒度(对于服务器,这通常是每个请求)。您可以在使用模式和最佳实践中了解有关如何充分利用竞技场分配的更多信息。
此表总结了使用竞技场的典型性能优势和劣势
操作 | 堆分配的 proto 消息 | 竞技场分配的 proto 消息 |
---|---|---|
消息分配 | 平均速度较慢 | 平均速度更快 |
消息销毁 | 平均速度较慢 | 平均速度更快 |
消息移动 | 始终移动(在成本方面等效于浅拷贝) | 有时是深拷贝 |
入门
Protocol Buffer 编译器为您的文件中使用的消息生成竞技场分配代码,如以下示例所示。
#include <google/protobuf/arena.h>
{
google::protobuf::Arena arena;
MyMessage* message = google::protobuf::Arena::CreateMessage<MyMessage>(&arena);
// ...
}
由CreateMessage()
创建的消息对象在arena
存在期间一直存在,并且您不应该delete
返回的消息指针。所有消息对象的内部存储(除了少数例外1)和子消息(例如,MyMessage
中重复字段中的子消息)也都在竞技场上分配。
在大多数情况下,您的其余代码将与不使用竞技场分配时相同。
我们将在以下部分更详细地介绍竞技场 API,您可以在文档末尾看到更广泛的示例。
Arena 类 API
您使用google::protobuf::Arena
类在竞技场上创建消息对象。此类实现了以下公共方法。
构造函数
Arena()
:使用默认参数创建一个新的竞技场,针对平均使用案例进行了调整。Arena(const ArenaOptions& options)
:创建一个使用指定分配选项的新竞技场。ArenaOptions
中可用的选项包括在求助于系统分配器之前使用用户提供的内存的初始块进行分配的能力,控制内存块的初始和最大请求大小,以及允许您传入自定义块分配和释放函数指针以构建自由列表以及在块之上构建其他内容。
分配方法
template<typename T> static T* CreateMessage(Arena* arena)
:在竞技场上创建一个消息类型为T
的新 Protocol Buffer 对象。如果
arena
不为 NULL,则返回的消息对象将在竞技场上分配,其内部存储和子消息(如果有)将在同一竞技场上分配,并且其生命周期与竞技场相同。该对象不得手动删除/释放:竞技场出于生命周期目的拥有该消息对象。如果
arena
为 NULL,则返回的消息对象将在堆上分配,并且调用方在返回时拥有该对象。template<typename T> static T* Create(Arena* arena, args...)
:类似于CreateMessage()
,但允许您在竞技场上创建任何类的对象,而不仅仅是 Protocol Buffer 消息类型。例如,假设您有以下 C++ 类class MyCustomClass { MyCustomClass(int arg1, int arg2); // ... };
…您可以在竞技场上这样创建它的实例
void func() { // ... google::protobuf::Arena arena; MyCustomClass* c = google::protobuf::Arena::Create<MyCustomClass>(&arena, constructor_arg1, constructor_arg2); // ... }
template<typename T> static T* CreateArray(Arena* arena, size_t n)
:如果arena
不为 NULL,则此方法为类型为T
的n
个元素分配原始存储并返回它。竞技场拥有返回的内存,并在其自身销毁时释放它。如果arena
为 NULL,则此方法在堆上分配存储,并且调用方获得所有权。T
必须具有一个平凡的构造函数:在竞技场上创建数组时不会调用构造函数。
“拥有列表”方法
以下方法允许您指定特定对象或析构函数由竞技场“拥有”,确保在竞技场本身被删除时删除或调用它们
template<typename T> void Own(T* object)
:将object
添加到竞技场的已拥有堆对象列表中。当竞技场被销毁时,它会遍历此列表并使用 operator delete 释放每个对象,即系统内存分配器。此方法在对象的生命周期应与竞技场绑定但由于某种原因,对象本身无法或尚未在竞技场上分配的情况下很有用。template<typename T> void OwnDestructor(T* object)
:将object
的析构函数添加到竞技场的要调用的析构函数列表中。当竞技场被销毁时,它会遍历此列表并依次调用每个析构函数。它不会尝试释放对象的底层内存。此方法在对象嵌入在竞技场分配的存储中但其析构函数不会被调用时很有用,例如因为其包含类是其析构函数不会被调用的 protobuf 消息,或者因为它是在AllocateArray()
分配的块中手动构造的。
其他方法
uint64 SpaceUsed() const
:返回竞技场的总大小,即底层块的大小之和。此方法是线程安全的;但是,如果有多个线程并发分配,则此方法的返回值可能不包含这些新块的大小。uint64 Reset()
:销毁竞技场的存储,首先调用所有已注册的析构函数并释放所有已注册的堆对象,然后丢弃所有竞技场块。此拆卸过程等效于竞技场的析构函数运行时发生的拆卸过程,只是此方法返回后竞技场可用于新分配。返回竞技场使用的总大小:此信息对于调整性能很有用。template<typename T> Arena* GetArena()
:返回指向此竞技场的指针。本身没什么用,但允许Arena
用于期望GetArena()
方法存在的模板实例化。
线程安全
google::protobuf::Arena
的分配方法是线程安全的,并且底层实现会尽力使多线程分配速度更快。Reset()
方法**不是**线程安全的:执行竞技场重置的线程必须首先与所有执行分配或使用从该竞技场分配的对象的线程同步。
生成的 Message 类
启用竞技场分配后,将更改或添加以下消息类成员。
Message 类方法
Message(Message&& other)
:如果源消息不在竞技场上,则移动构造函数会有效地将所有字段从一个消息移动到另一个消息,而无需进行复制或堆分配(此操作的时间复杂度为O(number-of-declared-fields)
)。但是,如果源消息在竞技场上,它会执行底层数据的深拷贝。在这两种情况下,源消息都将保留在有效但未指定的状态。Message& operator=(Message&& other)
:如果两个消息都不在 arena 上或都在**同一个** arena 上,移动赋值运算符会高效地将所有字段从一个消息**移动**到另一个消息,而无需进行复制或堆分配(此操作的时间复杂度为O(number-of-declared-fields)
)。但是,如果只有一个消息在 arena 上,或者消息在不同的 arena 上,则它会执行底层数据的**深拷贝**。在这两种情况下,源消息都将处于有效但未指定的状态。void Swap(Message* other)
:如果要交换的两个消息都不在 arena 上或都在**同一个** arena 上,Swap()
的行为与未启用 arena 分配时相同:它有效地交换消息对象的內容,几乎完全通过廉价的指针交换来实现,避免了复制。但是,如果只有一个消息在 arena 上,或者消息在不同的 arena 上,Swap()
会执行底层数据的**深拷贝**。这种新的行为是必要的,否则交换的子对象可能具有不同的生命周期,从而可能导致使用后释放的错误。Message* New(Arena* arena)
:标准New()
方法的替代重写。它允许在给定的 arena 上创建一个此类型的新消息对象。如果在其上调用的具体消息类型启用了 arena 分配,则其语义与Arena::CreateMessage<T>(arena)
相同。如果消息类型未启用 arena 分配,则它等效于普通分配,然后是arena->Own(message)
(如果arena
不为 NULL)。Arena* GetArena()
:返回此消息对象分配到的 arena(如果有)。void UnsafeArenaSwap(Message* other)
:与Swap()
相同,但它假设两个对象都在同一个 arena 上(或根本不在 arena 上),并且始终使用此操作的高效指针交换实现。使用此方法可以提高性能,因为它与Swap()
不同,在执行交换之前不需要检查哪些消息位于哪个 arena 上。如Unsafe
前缀所暗示的那样,只有在您确定要交换的消息不在不同的 arena 上时才应使用此方法;否则此方法可能会产生不可预测的结果。
嵌入式 Message 字段
当您在 arena 上分配消息对象时,其嵌入的消息字段对象(子消息)也会自动由 arena 拥有。这些消息对象的分配方式取决于它们在何处定义。
- 如果消息类型也在启用了 arena 分配的
.proto
文件中定义,则该对象会直接在 arena 上分配。 - 如果消息类型来自另一个未启用 arena 分配的
.proto
,则该对象将在堆上分配,但由父消息的 arena “拥有”。这意味着当 arena 被销毁时,该对象将与 arena 本身上的对象一起被释放。
对于这两个字段定义中的任何一个
optional Bar foo = 1;
required Bar foo = 1;
启用 arena 分配时,将添加以下方法或具有一些特殊行为。否则,访问器方法仅使用默认行为。
Bar* mutable_foo()
:返回指向子消息实例的可变指针。如果父对象在 arena 上,则返回的对象也将在此 arena 上。void set_allocated_foo(Bar* bar)
:获取一个新对象并将其作为字段的新值。当对象跨越 arena/arena 或 arena/heap 边界时,arena 支持会添加额外的复制语义以维护正确的拥有权。- 如果父对象在堆上并且
bar
在堆上,或者如果父对象和消息在同一个 arena 上,则此方法的行为不变。 - 如果父对象在 arena 上并且
bar
在堆上,则父消息会使用arena->Own()
将bar
添加到其 arena 的所有权列表中。 - 如果父对象在 arena 上并且
bar
在不同的 arena 上,则此方法会复制消息,并将副本作为新的字段值。
- 如果父对象在堆上并且
Bar* release_foo()
:如果已设置,则返回字段的现有子消息实例,否则返回 NULL 指针(如果未设置),将此实例的所有权释放给调用方并清除父消息的字段。arena 支持会添加额外的复制语义以维护返回的对象始终**在堆上分配**的约定。- 如果父消息在 arena 上,则此方法将在堆上复制子消息,清除字段值并返回副本。
- 如果父消息在堆上,则方法行为不变。
void unsafe_arena_set_allocated_foo(Bar* bar)
:与set_allocated_foo
相同,但假设父对象和子消息都在同一个 arena 上。使用此方法版本可以提高性能,因为它不需要检查消息是否在特定 arena 或堆上。有关安全使用此方法的详细信息,请参阅allocated/release 模式。Bar* unsafe_arena_release_foo()
:类似于release_foo()
,但跳过所有所有权检查。有关安全使用此方法的详细信息,请参阅allocated/release 模式。
字符串字段
即使其父消息在 arena 上,字符串字段也会将其数据存储在堆上。因此,即使启用了 arena 分配,字符串访问器方法也会使用默认行为。
重复字段
当包含的消息是 arena 分配时,重复字段会在 arena 上分配其内部数组存储,并且当这些元素是由指针(消息或字符串)保留的单独对象时,也会在 arena 上分配其元素。在消息类级别,重复字段的生成方法不会更改。但是,当启用 arena 支持时,由访问器返回的 RepeatedField
和 RepeatedPtrField
对象确实具有新方法和修改后的语义。
重复数值字段
启用 arena 分配时,包含基本类型的 RepeatedField
对象具有以下新/更改方法
void UnsafeArenaSwap(RepeatedField* other)
:执行RepeatedField
内容的交换,而不验证此重复字段和其他字段是否在同一个 arena 上。如果它们不在同一个 arena 上,则这两个重复字段对象必须在具有相同生命周期的 arena 上。检查并禁止其中一个在 arena 上而另一个在堆上的情况。void Swap(RepeatedField* other)
:检查每个重复字段对象所属的 arena,如果一个在 arena 上而另一个在堆上,或者如果两者都在 arena 上但在不同的 arena 上,则在交换发生之前会复制底层数组。这意味着交换后,每个重复字段对象都将持有其自身 arena 或堆上的数组,具体取决于情况。
重复嵌入消息字段
启用 arena 分配时,包含消息的 RepeatedPtrField
对象具有以下新/更改方法。
void UnsafeArenaSwap(RepeatedPtrField* other)
:执行RepeatedPtrField
内容的交换,而不验证此重复字段和其他字段是否具有相同的 arena 指针。如果它们不相同,则这两个重复字段对象必须具有生命周期相同的 arena 指针。检查并禁止其中一个具有非 NULL arena 指针而另一个具有 NULL arena 指针的情况。void Swap(RepeatedPtrField* other)
:检查每个重复字段对象的 arena 指针,如果一个是非 NULL(内容在 arena 上)而另一个是 NULL(内容在堆上),或者如果两者都是非 NULL 但具有不同的值,则在交换发生之前会复制底层数组及其指向的对象。这意味着交换后,每个重复字段对象都将持有其自身 arena 或堆上的数组,具体取决于情况。void AddAllocated(SubMessageType* value)
:检查提供的消息对象是否与重复字段的 arena 指针位于同一个 arena 上。- 源和目标都是 arena 分配并在同一个 arena 上:对象指针将直接添加到底层数组。
- 源和目标都是 arena 分配但在不同的 arena 上:进行复制,如果原始对象是在堆上分配的,则将其释放,并将副本放置在数组上。
- 源在堆上分配,目标在 arena 上分配:不进行复制。
- 源在 arena 上分配,目标在堆上分配:进行复制并将副本放置在数组上。
- 源和目标都在堆上分配:对象指针将直接添加到底层数组。
这保持了一个不变式,即重复字段指向的所有对象都与重复字段的 arena 指针指示的相同所有权域(堆或特定 arena)位于同一域。
SubMessageType* ReleaseLast()
:返回等效于重复字段中最后一个消息的堆分配消息,并将其从重复字段中删除。如果重复字段本身具有 NULL arena 指针(因此,其所有指向的消息都在堆上分配),则此方法仅返回指向原始对象的指针。否则,如果重复字段具有非 NULL arena 指针,则此方法会创建一个堆分配的副本并返回该副本。在这两种情况下,调用方都将获得堆分配对象的拥有权,并负责删除该对象。void UnsafeArenaAddAllocated(SubMessageType* value)
:类似于AddAllocated()
,但不执行堆/arena 检查或任何消息复制。它将提供的指针直接添加到此重复字段的内部指针数组中。有关安全使用此方法的详细信息,请参阅allocated/release 模式。SubMessageType* UnsafeArenaReleaseLast()
:类似于ReleaseLast()
,但不执行任何复制,即使重复字段具有非 NULL arena 指针也是如此。相反,它直接返回指向对象在重复字段中的指针。有关安全使用此方法的详细信息,请参阅allocated/release 模式。void ExtractSubrange(int start, int num, SubMessageType** elements)
:从重复字段中删除从索引start
开始的num
个元素,如果elements
不为 NULL,则将其返回到elements
中。如果重复字段在 arena 上并且正在返回元素,则会先将元素复制到堆上。在这两种情况下(arena 或无 arena),调用方都拥有堆上返回的对象。void UnsafeArenaExtractSubrange(int start, int num, SubMessageType** elements)
:从重复字段中删除从索引start
开始的num
个元素,如果elements
不为 NULL,则将其返回到elements
中。与ExtractSubrange()
不同,此方法永远不会复制提取的元素。有关安全使用此方法的详细信息,请参阅allocated/release 模式。
重复字符串字段
字符串的重复字段具有与消息的重复字段相同的新方法和修改后的语义,因为它们也通过指针引用维护其底层对象(即字符串)。
使用模式和最佳实践
当使用 arena 分配的消息时,一些使用模式会导致意外的复制或其他负面的性能影响。您应该注意以下在将代码适配到 arena 时可能需要更改的常见模式。(请注意,我们在 API 设计中已经注意确保正确行为仍然发生——但更高的性能解决方案可能需要一些修改。)
意外复制
一些在不使用 arena 分配时永远不会创建对象副本的方法,在启用 arena 支持后可能会最终执行此操作。如果您确保您的对象被适当地分配和/或使用提供的 arena 特定方法版本(如下面更详细地描述),则可以避免这些不需要的副本。
Set Allocated/Add Allocated/Release
默认情况下,release_field()
和 set_allocated_field()
方法(用于单个消息字段),以及 ReleaseLast()
和 AddAllocated()
方法(用于重复消息字段)允许用户代码直接附加和分离子消息,在不复制任何数据的情况下传递指针的所有权。
但是,当父消息位于 arena 上时,这些方法现在有时需要复制传入或返回的对象以保持与现有所有权契约的兼容性。更具体地说,获取所有权的方法(set_allocated_field()
和 AddAllocated()
)如果父级位于 arena 上而新子对象不在,或者反之亦然,或者它们位于不同的 arena 上,则可能会复制数据。释放所有权的方法(release_field()
和 ReleaseLast()
)如果父级位于 arena 上,则可能会复制数据,因为根据契约,返回的对象必须位于堆上。
为了避免此类复制,我们添加了这些方法的相应“不安全 arena”版本,其中**永远不会执行**复制:unsafe_arena_set_allocated_field()
、unsafe_arena_release_field()
、UnsafeArenaAddAllocated()
和 UnsafeArenaRelease()
分别用于单一和重复字段。这些方法应该只在您知道这样做是安全的情况下使用。这些方法有两种常见的模式
- 在同一 arena 的不同部分之间移动消息树。请注意,对于这种情况的安全,消息必须位于同一 arena 上。
- 临时将拥有的消息借给树以避免复制。将不安全的 add/set 方法与不安全的 release 方法配对,可以以最便宜的方式执行借用,而不管消息如何拥有(当它们位于同一 arena、不同 arena 或根本没有 arena 上时,此模式都适用)。请注意,在不安全的 add/set 及其对应的 release 之间,借用方不得交换、移动、清除或销毁;借用的消息不得交换或移动;借用的消息不得被借用方清除或释放;并且借用的消息不得被销毁。
以下是如何使用这些方法避免不必要的复制的示例。假设您在 arena 上创建了以下消息。
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);
以下代码对 release_...()
API 进行了低效的使用
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”版本可以避免复制
arena_message_2->unsafe_arena_set_allocated_nested_message(
arena_message_1->unsafe_arena_release_nested_message());
您可以在上面的嵌入消息字段部分中找到有关这些方法的更多信息。
Swap
当两个消息的内容使用 Swap()
交换时,如果两个消息位于不同的 arena 上,或者一个位于 arena 上而另一个位于堆上,则底层子对象可能会被复制。如果您想避免此复制,并且 (i) 知道两个消息位于同一 arena 上或不同的 arena 上,但 arena 具有等效的生命周期,或者 (ii) 知道两个消息位于堆上,您可以使用一种新方法 UnsafeArenaSwap()
。此方法既避免了执行 arena 检查的开销,又在发生复制时避免了复制。
例如,以下代码在 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!
为了避免此代码中的复制,您可以在与 message_1
相同的 arena 上分配 message_2
MyFeatureMessage* message_2 =
google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);
粒度
我们发现在大多数应用程序服务器用例中,“每个请求一个 arena”模型效果很好。您可能会倾向于进一步划分 arena 的使用,要么减少堆开销(通过更频繁地销毁较小的 arena),要么减少感知到的线程争用问题。但是,如上所述,使用更细粒度的 arena 可能会导致意外的消息复制。我们还努力优化了 Arena
的实现以适应多线程用例,因此即使多个线程处理该请求,单个 arena 也应该适合在整个请求生命周期中使用。
示例
这是一个简单的完整示例,演示了 arena 分配 API 的一些功能。
// my_feature.proto
syntax = "proto2";
import "nested_message.proto";
package feature_package;
// NEXT Tag to use: 4
message MyFeatureMessage {
optional string feature_name = 1;
repeated int32 feature_data = 2;
optional NestedMessage nested_message = 3;
};
// nested_message.proto
syntax = "proto2";
package feature_package;
// NEXT Tag to use: 2
message NestedMessage {
optional int32 feature_id = 1;
};
消息构建和释放
#include <google/protobuf/arena.h>
Arena arena;
MyFeatureMessage* arena_message =
google::protobuf::Arena::CreateMessage<MyFeatureMessage>(&arena);
arena_message->set_feature_name("Proto2 Arena");
arena_message->mutable_feature_data()->Add(2);
arena_message->mutable_feature_data()->Add(4);
arena_message->mutable_nested_message()->set_feature_id(247);
目前,即使包含消息位于 arena 上,字符串字段也会将其数据存储在堆上。未知字段也会被堆分配。 ↩︎