C++ Arena 配置指南

Arena 配置是 C++ 獨有的功能,可協助您最佳化記憶體用量,並提升使用 Protocol Buffers 時的效能。

本頁說明在啟用 Arena 配置時,除了 C++ 產生程式碼指南 中說明的程式碼之外,Protocol Buffer 編譯器實際產生的 C++ 程式碼。本頁假設您已熟悉語言指南C++ 產生程式碼指南中的資料。

為何使用 Arena 配置?

在 Protocol Buffers 程式碼中,記憶體配置和解除配置佔用了大量的 CPU 時間。根據預設,Protocol Buffers 會為每個訊息物件、其每個子物件以及數種欄位類型 (例如字串) 執行堆積配置。這些配置會在剖析訊息以及在記憶體中建立新訊息時大量發生,而相關的解除配置則會在訊息及其子物件樹狀結構釋出時發生。

基於 Arena 的配置旨在降低此效能成本。透過 Arena 配置,新物件會從稱為 Arena 的大型預先配置記憶體區塊中配置出來。物件可以透過捨棄整個 Arena 一次全部釋出,理想情況下無需執行任何包含物件的解構函式 (不過,Arena 仍可在需要時維護「解構函式清單」)。這可將物件配置簡化為簡單的指標遞增,從而加快物件配置速度,並使解除配置幾乎免費。Arena 配置也提供更高的快取效率:剖析訊息時,訊息更有可能配置在連續記憶體中,這使得遍歷訊息更有可能命中熱快取行。

為了獲得這些優勢,您需要注意物件生命週期,並找到適合使用 Arena 的粒度 (對於伺服器,這通常是每個請求)。您可以在使用模式與最佳實務中找到更多關於如何充分利用 Arena 配置的資訊。

下表總結了使用 Arena 的典型效能優勢和劣勢

操作堆積配置的 Proto 訊息Arena 配置的 Proto 訊息
訊息配置平均較慢平均較快
訊息解構平均較慢平均較快
訊息移動始終是移動 (相當於成本中的淺層複製)有時是深層複製

開始使用

Protocol Buffer 編譯器會為您檔案中的訊息產生 Arena 配置的程式碼,如下列範例所示。

#include <google/protobuf/arena.h>
{
  google::protobuf::Arena arena;
  MyMessage* message = google::protobuf::Arena::Create<MyMessage>(&arena);
  // ...
}

Create() 建立的訊息物件會在 arena 存在期間存在,您不應 delete 傳回的訊息指標。所有訊息物件的內部儲存空間 (少數例外1) 和子訊息 (例如,MyMessage 中重複欄位中的子訊息) 也會配置在 Arena 上。

在大多數情況下,其餘程式碼與您未使用 Arena 配置時相同。

我們將在以下章節中更詳細地探討 Arena API,您可以在文件末尾看到更廣泛的範例

Arena 類別 API

您可以使用 google::protobuf::Arena 類別在 Arena 上建立訊息物件。此類別實作了以下公用方法。

建構函式

  • Arena():使用預設參數建立新的 Arena,針對一般用例進行調整。
  • Arena(const ArenaOptions& options):建立新的 Arena,其使用指定的配置選項。ArenaOptions 中可用的選項包括在求助於系統配置器之前,使用使用者提供的初始記憶體區塊進行配置的能力、控制記憶體區塊的初始和最大請求大小,以及允許您傳入自訂區塊配置和解除配置函式指標,以在區塊之上建立可用空間清單和其他項目。

配置方法

  • template<typename T> static T* Create(Arena* arena)template<typename T> static T* Create(Arena* arena, args...)

    • 如果 T 完全相容2,則此方法會在 Arena 上建立類型 T 的新 Protocol Buffer 物件及其子物件。

      如果 arena 不是 NULL,則傳回的物件會配置在 Arena 上,其內部儲存空間和子類型 (如果有的話) 也會配置在同一個 Arena 上,且其生命週期與 Arena 的生命週期相同。物件不得手動刪除/釋出:Arena 擁有物件的生命週期目的。

      如果 arena 為 NULL,則傳回的物件會配置在堆積上,且呼叫者會在傳回時擁有物件。

    • 如果 T 是使用者類型,則此方法可讓您建立物件,但不會在 Arena 上建立子物件。例如,假設您有這個 C++ 類別

      class MyCustomClass {
          MyCustomClass(int arg1, int arg2);
          // ...
      };
      

      …您可以像這樣在 Arena 上建立它的執行個體

      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,則此方法會為類型 Tn 個元素配置原始儲存空間並傳回它。Arena 擁有傳回的記憶體,並會在自行解構時釋出它。如果 arena 為 NULL,則此方法會在堆積上配置儲存空間,且呼叫者會收到所有權。

    T 必須具有簡單的建構函式:在 Arena 上建立陣列時,不會呼叫建構函式。

「擁有清單」方法

以下方法可讓您指定特定物件或解構函式由 Arena「擁有」,確保它們在 Arena 本身刪除時被刪除或呼叫

  • template<typename T> void Own(T* object):將 object 新增至 Arena 的擁有堆積物件清單。當 Arena 被解構時,它會遍歷此清單,並使用運算子 delete (即系統記憶體配置器) 釋出每個物件。當物件的生命週期應與 Arena 綁定,但由於任何原因,物件本身無法或尚未配置在 Arena 上時,此方法非常有用。
  • template<typename T> void OwnDestructor(T* object):將 object 的解構函式新增至 Arena 的解構函式清單以進行呼叫。當 Arena 被解構時,它會遍歷此清單,並依序呼叫每個解構函式。它不會嘗試釋出物件的底層記憶體。當物件嵌入在 Arena 配置的儲存空間中,但其解構函式不會以其他方式呼叫時,此方法非常有用,例如因為其包含類別是 Protocol Buffer 訊息,其解構函式不會被呼叫,或者因為它是手動在 AllocateArray() 配置的區塊中建構的。

其他方法

  • uint64 SpaceUsed() const:傳回 Arena 的總大小,這是底層區塊大小的總和。此方法是執行緒安全的;但是,如果有多個執行緒同時進行配置,則此方法的傳回值可能不包含這些新區塊的大小。
  • uint64 Reset():解構 Arena 的儲存空間,首先呼叫所有已註冊的解構函式並釋出所有已註冊的堆積物件,然後捨棄所有 Arena 區塊。此拆解程序等同於 Arena 的解構函式執行時發生的程序,不同之處在於 Arena 在此方法傳回後可重複用於新的配置。傳回 Arena 使用的總大小:此資訊對於調整效能很有用。
  • template<typename T> Arena* GetArena():傳回指向此 Arena 的指標。並非直接非常有用,但允許在期望存在 GetArena() 方法的範本具現化中使用 Arena

執行緒安全

google::protobuf::Arena 的配置方法是執行緒安全的,且底層實作會盡力使多執行緒配置快速。Reset() 方法不是執行緒安全的:執行 Arena 重設的執行緒必須先與執行配置或使用從該 Arena 配置的物件的所有執行緒同步。

產生的訊息類別

當您啟用 Arena 配置時,以下訊息類別成員會變更或新增。

訊息類別方法

  • Message(Message&& other):如果來源訊息不在 Arena 上,則移動建構函式會有效率地移動一個訊息到另一個訊息的所有欄位,而無需進行複製或堆積配置 (此操作的時間複雜度為 O(宣告欄位數))。但是,如果來源訊息在 Arena 上,則它會執行底層資料的深層複製。在這兩種情況下,來源訊息都處於有效但不確定的狀態。
  • Message& operator=(Message&& other):如果兩個訊息都不在 Arena 上,或在相同 Arena 上,則移動指派運算子會有效率地移動一個訊息到另一個訊息的所有欄位,而無需進行複製或堆積配置 (此操作的時間複雜度為 O(宣告欄位數))。但是,如果只有一個訊息在 Arena 上,或訊息在不同的 Arena 上,則它會執行底層資料的深層複製。在這兩種情況下,來源訊息都處於有效但不確定的狀態。
  • void Swap(Message* other):如果要交換的兩個訊息都不在 Arena 上,或在相同 Arena 上,則 Swap() 的行為與未啟用 Arena 配置時相同:它會有效率地交換訊息物件的內容,幾乎完全透過廉價的指標交換,避免複製。但是,如果只有一個訊息在 Arena 上,或訊息在不同的 Arena 上,則 Swap() 會執行底層資料的深層複製。此新行為是必要的,因為否則交換的子物件可能會具有不同的生命週期,從而可能導致使用後釋放錯誤。
  • Message* New(Arena* arena):標準 New() 方法的替代覆寫。它允許在此指定的 Arena 上建立此類型的新訊息物件。如果呼叫它的具體訊息類型是透過啟用 Arena 配置產生的,則其語意與 Arena::Create<T>(arena) 相同。如果訊息類型不是透過啟用 Arena 配置產生的,則如果 arena 不是 NULL,則它等同於普通配置後接續 arena->Own(message)
  • Arena* GetArena():傳回此訊息物件配置所在的 Arena (如果有的話)。
  • void UnsafeArenaSwap(Message* other):與 Swap() 相同,但它假設兩個物件都在相同的 Arena 上 (或根本不在 Arena 上),並且始終使用此操作的有效率指標交換實作。使用此方法可以提高效能,因為與 Swap() 不同,它不需要檢查哪些訊息位於哪個 Arena 上才能執行交換。如同 Unsafe 前置詞所暗示的,只有在您確定要交換的訊息不在不同的 Arena 上時,才應使用此方法;否則此方法可能會產生無法預測的結果。

嵌入訊息欄位

當您在 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 或 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 或堆積上。請參閱配置/釋出模式以取得關於安全使用此方法的詳細資訊。
  • Bar* unsafe_arena_release_foo():與 release_foo() 類似,但會略過所有所有權檢查。請參閱配置/釋出模式以取得關於安全使用此方法的詳細資訊。

字串欄位

即使字串欄位的父訊息在 Arena 上,字串欄位也會將其資料儲存在堆積上。因此,即使啟用 Arena 配置,字串存取器方法也會使用預設行為

重複欄位

重複欄位會在包含訊息進行 Arena 配置時,將其內部陣列儲存空間配置在 Arena 上,並且在這些元素是由指標 (訊息或字串) 保留的個別物件時,也會將其元素配置在 Arena 上。在訊息類別層級,重複欄位的產生方法不會變更。但是,由存取器傳回的 RepeatedFieldRepeatedPtrField 物件在啟用 Arena 支援時,確實具有新的方法和修改的語意。

重複數值欄位

當啟用 Arena 配置時,包含基本類型的 RepeatedField 物件具有以下新的/已變更方法

  • void UnsafeArenaSwap(RepeatedField* other):執行 RepeatedField 內容的交換,而無需驗證此重複欄位和其他欄位是否在相同的 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 檢查或任何訊息副本。它會將提供的指標直接新增至此重複欄位的指標內部陣列。請參閱配置/釋出模式以取得關於安全使用此方法的詳細資訊。

  • SubMessageType* UnsafeArenaReleaseLast():與 ReleaseLast() 類似,但不執行任何副本,即使重複欄位具有非 NULL Arena 指標也是如此。相反地,它會直接傳回物件的指標,如同它在重複欄位中一樣。請參閱配置/釋出模式以取得關於安全使用此方法的詳細資訊。

  • 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() 不同,此方法永遠不會複製擷取的元素。請參閱配置/釋出模式以取得關於安全使用此方法的詳細資訊。

重複字串欄位

字串的重複欄位具有與訊息的重複欄位相同的新方法和修改的語意,因為它們也透過指標參考來維護其底層物件 (即字串)。

使用模式與最佳實務

當使用 Arena 配置的訊息時,數種使用模式可能會導致非預期的複製或其他負面效能影響。您應注意以下常見模式,這些模式在調整程式碼以適用於 Arena 時可能需要變更。(請注意,我們已在 API 設計中謹慎處理,以確保仍然發生正確的行為 — 但效能更高的解決方案可能需要進行一些修改。)

非預期的複製

當未使用 Arena 配置時,數種永遠不會建立物件副本的方法在啟用 Arena 支援時最終可能會這樣做。如果您確保物件已適當配置和/或使用提供的 Arena 特定方法版本,則可以避免這些不必要的副本,如下文更詳細的說明。

設定配置/新增配置/釋出

根據預設,release_field()set_allocated_field() 方法 (對於單數訊息欄位),以及 ReleaseLast()AddAllocated() 方法 (對於重複訊息欄位) 允許使用者程式碼直接附加和分離子訊息,傳遞指標的所有權而無需複製任何資料。

但是,當父訊息在 Arena 上時,這些方法現在有時需要複製傳入或傳回的物件,以維持與現有所有權合約的相容性。更具體地說,取得所有權的方法 (set_allocated_field()AddAllocated()) 如果父物件在 Arena 上且新的子物件不在 Arena 上,反之亦然,或者它們在不同的 Arena 上,則可能會複製資料。釋出所有權的方法 (release_field()ReleaseLast()) 如果父物件在 Arena 上,則可能會複製資料,因為根據合約,傳回的物件必須在堆積上。

為了避免此類複製,我們新增了這些方法的對應「不安全 Arena」版本,在這些版本中永遠不會執行複製:單數和重複欄位的 unsafe_arena_set_allocated_field()unsafe_arena_release_field()UnsafeArenaAddAllocated()UnsafeArenaRelease()。只有在您知道它們可以安全使用時,才應使用這些方法。這些方法有兩種常見模式

  • 在相同 Arena 的不同部分之間移動訊息樹狀結構。請注意,訊息必須在相同的 Arena 上,此情況才是安全的。
  • 暫時將擁有的訊息借給樹狀結構以避免複製。將不安全新增/設定方法與不安全釋出方法配對,以最便宜的方式執行借用,無論任何訊息的所有權方式為何 (當它們在相同的 Arena、不同的 Arena 或根本不在 Arena 上時,此模式都有效)。請注意,在不安全新增/設定及其對應的釋出之間,借用者不得交換、移動、清除或解構;借出的訊息不得交換或移動;借出的訊息不得由借用者清除或釋出;且借出的訊息不得解構。

以下範例說明如何使用這些方法避免不必要的複製。假設您已在 Arena 上建立以下訊息。

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

MyFeatureMessage* arena_message_2 =
  google::protobuf::Arena::Create<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() 交換時,如果兩個訊息位於不同的 Arena 上,或者如果一個在 Arena 上而另一個在堆積上,則可能會複製底層子物件。如果您想要避免此複製,並且 (i) 知道兩個訊息在相同的 Arena 上或不同的 Arena 上,但 Arena 具有等效的生命週期,或者 (ii) 知道兩個訊息在堆積上,則可以使用新方法 UnsafeArenaSwap()。此方法既避免了執行 Arena 檢查的額外負荷,又避免了在可能發生的情況下進行複製。

例如,以下程式碼會在 Swap() 呼叫中產生複製

MyFeatureMessage* message_1 =
  google::protobuf::Arena::Create<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::Create<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::Create<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);

  1. 目前,即使字串欄位的包含訊息在 Arena 上,字串欄位也會將其資料儲存在堆積上。未知欄位也是堆積配置的。 ↩︎

  2. 成為「完全相容」類型需要什麼是 Protocol Buffer 程式庫的內部資訊,不應假設為可靠。 ↩︎