C++ Arena Allocation ガイド

Arena allocationは、protocol buffersを扱う際のメモリ使用量を最適化し、パフォーマンスを向上させるのに役立つC++専用の機能です。

このページでは、arena allocationが有効になっている場合に、C++生成コードガイドで説明されているコードに加えて、protocol bufferコンパイラが具体的にどのようなC++コードを生成するかを説明します。読者は言語ガイドC++生成コードガイドの内容に精通していることを前提としています。

Arena Allocationを使用する理由

メモリの確保と解放は、protocol buffersのコードで費やされるCPU時間のかなりの部分を占めます。デフォルトでは、protocol buffersは各メッセージオブジェクト、その各サブオブジェクト、および文字列などのいくつかのフィールドタイプに対してヒープアロケーションを実行します。これらのアロケーションは、メッセージをパースするときや新しいメッセージをメモリ上に構築するときにまとめて発生し、関連するデアロケーションは、メッセージとそのサブオブジェクトツリーが解放されるときに発生します。

Arenaベースのアロケーションは、このパフォーマンスコストを削減するために設計されました。arena allocationでは、新しいオブジェクトはアリーナと呼ばれる事前に確保された大きなメモリ領域から割り当てられます。オブジェクトは、アリーナ全体を破棄することですべて一度に解放できます。理想的には、含まれるオブジェクトのデストラクタを実行することなく解放できます(ただし、アリーナは必要に応じて「デストラクタリスト」を維持することもできます)。これにより、オブジェクトの確保は単純なポインタのインクリメントに短縮され、解放はほぼ無料になります。Arena allocationはまた、キャッシュ効率も向上させます。メッセージがパースされるとき、それらは連続したメモリに割り当てられる可能性が高いため、メッセージを走査する際にホットキャッシュラインにヒットする可能性が高くなります。

これらの利点を得るためには、オブジェクトのライフタイムを意識し、アリーナを使用する適切な粒度を見つける必要があります(サーバーの場合、これは多くの場合リクエストごとです)。arena allocationを最大限に活用する方法については、使用パターンとベストプラクティスで詳しく説明しています。

この表は、アリーナを使用する場合の典型的なパフォーマンスの利点と欠点をまとめたものです。

操作ヒープ確保されたprotoメッセージアリーナ確保されたprotoメッセージ
メッセージの確保平均して遅い平均して速い
メッセージの破棄平均して遅い平均して速い
メッセージのムーブ常にムーブ(コスト的にはシャローコピーに相当)時々ディープコピー

はじめに

protocol bufferコンパイラは、次の例で使用されるように、ファイル内のメッセージに対してarena allocation用のコードを生成します。

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

Create()によって作成されたメッセージオブジェクトは、arenaが存在する限り存在し、返されたメッセージポインタをdeleteしてはいけません。メッセージオブジェクトの内部ストレージ(いくつかの例外を除く1)およびサブメッセージ(例えば、MyMessage内のリピートフィールドのサブメッセージ)はすべて、同様にアリーナ上に確保されます。

ほとんどの場合、残りのコードはarena allocationを使用していない場合と同じになります。

次のセクションではアリーナAPIをさらに詳しく見ていきます。また、ドキュメントの最後にはより広範ながあります。

ArenaクラスAPI

アリーナ上にメッセージオブジェクトを作成するには、google::protobuf::Arenaクラスを使用します。このクラスは、以下のパブリックメソッドを実装しています。

コンストラクタ

  • Arena(): 平均的なユースケースに合わせて調整されたデフォルトパラメータで新しいアリーナを作成します。
  • Arena(const ArenaOptions& options): 指定されたアロケーションオプションを使用して新しいアリーナを作成します。ArenaOptionsで利用可能なオプションには、システムアロケータに頼る前に、ユーザーが提供した初期メモリブロックをアロケーションに使用する機能、メモリブロックの初期および最大リクエストサイズの制御、カスタムブロックアロケーションおよびデアロケーション関数ポインタを渡してフリーリストなどをブロック上に構築する機能などがあります。

アロケーションメソッド

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

    • Tが完全に互換性がある場合2、このメソッドはT型の新しいprotocol bufferオブジェクトとそのサブオブジェクトをアリーナ上に作成します。

      arenaがNULLでない場合、返されるオブジェクトはアリーナ上に確保され、その内部ストレージとサブタイプ(もしあれば)も同じアリーナ上に確保され、そのライフタイムはアリーナと同じになります。オブジェクトは手動でdelete/解放してはいけません。アリーナがライフタイムの目的でオブジェクトを所有します。

      arenaがNULLの場合、返されるオブジェクトはヒープ上に確保され、呼び出し元がオブジェクトの所有権を持ちます。

    • Tがユーザー定義型の場合、このメソッドはオブジェクトをアリーナ上に作成できますが、サブオブジェクトは作成しません。例えば、次のような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のデストラクタを追加します。アリーナが破棄されるとき、このリストを走査し、各デストラクタを順に呼び出します。objectの基になるメモリを解放しようとはしません。このメソッドは、オブジェクトがアリーナ確保されたストレージに埋め込まれているが、そのデストラクタが他の方法では呼び出されない場合に便利です。例えば、そのオブジェクトを含むクラスがprotobufメッセージで、そのデストラクタが呼び出されない場合や、AllocateArray()によって確保されたブロック内で手動で構築された場合などです。

その他のメソッド

  • uint64 SpaceUsed() const: アリーナの合計サイズを返します。これは、基になるブロックのサイズの合計です。このメソッドはスレッドセーフですが、複数のスレッドから同時にアロケーションがある場合、このメソッドの戻り値にはそれらの新しいブロックのサイズが含まれていない可能性があります。
  • uint64 Reset(): アリーナのストレージを破棄します。まず登録されたすべてのデストラクタを呼び出し、登録されたすべてのヒープオブジェクトを解放し、その後すべてのアリーナブロックを破棄します。この破棄手順は、アリーナのデストラクタが実行されるときに発生するものと同じですが、このメソッドが返った後、アリーナは新しいアロケーションのために再利用可能です。アリーナが使用した合計サイズを返します。この情報はパフォーマンスのチューニングに役立ちます。
  • template<typename T> Arena* GetArena(): このアリーナへのポインタを返します。直接的にはあまり有用ではありませんが、これによりArenaGetArena()メソッドの存在を期待するテンプレートのインスタンス化で使用できるようになります。

スレッドセーフティ

google::protobuf::Arenaのアロケーションメソッドはスレッドセーフであり、基盤となる実装はマルチスレッドのアロケーションを高速化するためにかなりの工夫がされています。Reset()メソッドはスレッドセーフでは*ありません*。アリーナのリセットを実行するスレッドは、そのアリーナからアロケーションを行う、または確保されたオブジェクトを使用するすべてのスレッドと、まず同期を取る必要があります。

生成されたメッセージクラス

arena allocationを有効にすると、以下のメッセージクラスのメンバが変更または追加されます。

メッセージクラスメソッド

  • Message(Message&& other): ソースメッセージがアリーナ上にない場合、ムーブコンストラクタはコピーやヒープアロケーションを行わずに、すべてのフィールドをあるメッセージから別のメッセージへ効率的に*ムーブ*します(この操作の時間計算量はO(宣言されたフィールド数)です)。しかし、ソースメッセージがアリーナ上にある場合、基になるデータの*ディープコピー*を実行します。どちらの場合も、ソースメッセージは有効ですが未指定の状態になります。
  • Message& operator=(Message&& other): 両方のメッセージがアリーナ上にないか、または*同じ*アリーナ上にある場合、ムーブ代入演算子はコピーやヒープアロケーションを行わずに、すべてのフィールドをあるメッセージから別のメッセージへ効率的に*ムーブ*します(この操作の時間計算量はO(宣言されたフィールド数)です)。しかし、片方のメッセージだけがアリーナ上にある場合、またはメッセージが異なるアリーナ上にある場合、基になるデータの*ディープコピー*を実行します。どちらの場合も、ソースメッセージは有効ですが未指定の状態になります。
  • void Swap(Message* other): 交換される両方のメッセージがアリーナ上にないか、または*同じ*アリーナ上にある場合、Swap()はarena allocationを有効にしていない場合と同様に動作します。つまり、メッセージオブジェクトの内容を、ほとんどの場合安価なポインタスワップによって効率的に交換し、コピーを回避します。しかし、片方のメッセージだけがアリーナ上にある場合、またはメッセージが異なるアリーナ上にある場合、Swap()は基になるデータの*ディープコピー*を実行します。この新しい挙動は、そうでなければ交換されたサブオブジェクトのライフタイムが異なる可能性があり、use-after-freeバグにつながる可能性があるため必要です。
  • Message* New(Arena* arena): 標準のNew()メソッドの代替オーバーライドです。これにより、指定されたアリーナ上にこの型の新しいメッセージオブジェクトを作成できます。これが呼び出された具象メッセージ型がarena allocationを有効にして生成された場合、そのセマンティクスはArena::Create<T>(arena)と同じです。メッセージ型がarena allocationを有効にして生成されていない場合、arenaがNULLでなければ、通常のアロケーションに続いてarena->Own(message)が実行されるのと同等です。
  • Arena* GetArena(): このメッセージオブジェクトが確保されたアリーナ(もしあれば)を返します。
  • void UnsafeArenaSwap(Message* other): Swap()と同じですが、両方のオブジェクトが同じアリーナ上にある(またはどちらもアリーナ上にない)と仮定し、常にこの操作の効率的なポインタスワップ実装を使用します。このメソッドを使用すると、Swap()とは異なり、スワップを実行する前にどのメッセージがどのアリーナ上にあるかを確認する必要がないため、パフォーマンスが向上する可能性があります。Unsafeという接頭辞が示すように、このメソッドは交換したいメッセージが異なるアリーナ上にないと確信している場合にのみ使用してください。さもなければ、このメソッドは予測不可能な結果をもたらす可能性があります。

埋め込みメッセージフィールド

メッセージオブジェクトをアリーナ上に確保すると、その埋め込みメッセージフィールドオブジェクト(サブメッセージ)も自動的にアリーナによって所有されます。これらのメッセージオブジェクトがどのように確保されるかは、それらがどこで定義されているかによります。

  • メッセージ型もarena allocationが有効な.protoファイルで定義されている場合、オブジェクトは直接アリーナ上に確保されます。
  • メッセージ型がarena allocationが有効でない別の.protoからのものである場合、オブジェクトはヒープ確保されますが、親メッセージのアリーナによって「所有」されます。これは、アリーナが破棄されるときに、オブジェクトがアリーナ自体のオブジェクトと一緒に解放されることを意味します。

フィールド定義について

Bar foo = 1;

以下のメソッドは、arena allocationが有効になっている場合に追加されるか、特別な動作をします。それ以外の場合、アクセサメソッドはデフォルトの動作を使用します。

  • Bar* mutable_foo(): サブメッセージインスタンスへの可変ポインタを返します。親オブジェクトがアリーナ上にある場合、返されるオブジェクトも同様にアリーナ上になります。
  • void set_allocated_foo(Bar* bar): 新しいオブジェクトを受け取り、それをフィールドの新しい値として採用します。アリーナのサポートは、オブジェクトがアリーナ/アリーナまたはアリーナ/ヒープの境界を越えるときに適切な所有権を維持するために、追加のコピーセマンティクスを追加します。
    • 親オブジェクトがヒープ上にあり、barがヒープ上にある場合、または親とメッセージが同じアリーナ上にある場合、このメソッドの動作は変更されません。
    • 親がアリーナ上にあり、barがヒープ上にある場合、親メッセージはarena->Own()barをそのアリーナの所有リストに追加します。
    • 親がアリーナ上にあり、barが異なるアリーナ上にある場合、このメソッドはメッセージのコピーを作成し、そのコピーを新しいフィールド値として受け取ります。
  • Bar* release_foo(): 設定されている場合、フィールドの既存のサブメッセージインスタンスを返します。設定されていない場合はNULLポインタを返します。このインスタンスの所有権を呼び出し元に解放し、親メッセージのフィールドをクリアします。アリーナのサポートは、返されるオブジェクトが常に*ヒープ確保*されているという契約を維持するために、追加のコピーセマンティクスを追加します。
    • 親メッセージがアリーナ上にある場合、このメソッドはサブメッセージのコピーをヒープ上に作成し、フィールド値をクリアして、そのコピーを返します。
    • 親メッセージがヒープ上にある場合、メソッドの動作は変更されません。
  • void unsafe_arena_set_allocated_foo(Bar* bar): set_allocated_fooと同じですが、親とサブメッセージが同じアリーナ上にあると仮定します。このバージョンのメソッドを使用すると、メッセージが特定のアリーナ上にあるかヒープ上にあるかを確認する必要がないため、パフォーマンスが向上する可能性があります。安全な使用方法の詳細については、allocated/releaseパターンを参照してください。
  • Bar* unsafe_arena_release_foo(): release_foo()に似ていますが、すべての所有権チェックをスキップします。安全な使用方法の詳細については、allocated/releaseパターンを参照してください。

文字列フィールド

文字列フィールドは、その親メッセージがアリーナ上にある場合でも、データをヒープ上に格納します。このため、arena allocationが有効になっている場合でも、文字列アクセサメソッドはデフォルトの動作を使用します。

繰り返しフィールド

リピートフィールドは、含まれるメッセージがアリーナ確保されている場合、その内部配列ストレージをアリーナ上に確保します。また、要素がポインタで保持される別個のオブジェクト(メッセージや文字列)である場合、それらの要素もアリーナ上に確保します。メッセージクラスレベルでは、リピートフィールド用に生成されたメソッドは変更されません。ただし、アクセサによって返されるRepeatedFieldおよびRepeatedPtrFieldオブジェクトは、アリーナサポートが有効になっている場合、新しいメソッドと変更されたセマンティクスを持ちます。

リピート数値フィールド

プリミティブ型を含むRepeatedFieldオブジェクトは、arena allocationが有効になっている場合、以下の新しい/変更されたメソッドを持ちます。

  • void UnsafeArenaSwap(RepeatedField* other): このリピートフィールドとotherが同じアリーナ上にあることを検証せずにRepeatedFieldの内容をスワップします。そうでない場合、2つのリピートフィールドオブジェクトは同等のライフタイムを持つアリーナ上になければなりません。片方がアリーナ上にあり、もう片方がヒープ上にある場合はチェックされ、許可されません。
  • void Swap(RepeatedField* other): 各リピートフィールドオブジェクトのアリーナをチェックし、片方がアリーナ上にあり、もう片方がヒープ上にある場合、または両方がアリーナ上にあるが異なるアリーナ上にある場合、スワップが行われる前に基になる配列がコピーされます。これは、スワップ後、各リピートフィールドオブジェクトが、適切に自身のアリーナまたはヒープ上に配列を保持することを意味します。

リピート埋め込みメッセージフィールド

メッセージを含むRepeatedPtrFieldオブジェクトは、arena allocationが有効になっている場合、以下の新しい/変更されたメソッドを持ちます。

  • void UnsafeArenaSwap(RepeatedPtrField* other): このリピートフィールドとotherが同じアリーナポインタを持つことを検証せずにRepeatedPtrFieldの内容をスワップします。そうでない場合、2つのリピートフィールドオブジェクトは同等のライフタイムを持つアリーナポインタを持たなければなりません。片方が非NULLのアリーナポインタを持ち、もう片方がNULLのアリーナポインタを持つ場合はチェックされ、許可されません。

  • void Swap(RepeatedPtrField* other): 各リピートフィールドオブジェクトのアリーナポインタをチェックし、片方が非NULL(内容はアリーナ上)で、もう片方がNULL(内容はヒープ上)である場合、または両方が非NULLであるが値が異なる場合、スワップが行われる前に基になる配列とそれが指すオブジェクトがコピーされます。これは、スワップ後、各リピートフィールドオブジェクトが、適切に自身のアリーナまたはヒープ上に配列を保持することを意味します。

  • void AddAllocated(SubMessageType* value): 提供されたメッセージオブジェクトがリピートフィールドのアリーナポインタと同じアリーナ上にあることをチェックします。

    • ソースとデスティネーションの両方がアリーナ確保されており、同じアリーナ上にある場合:オブジェクトポインタは直接基になる配列に追加されます。
    • ソースとデスティネーションの両方がアリーナ確保されており、異なるアリーナ上にある場合:コピーが作成され、元がヒープ確保されていた場合は解放され、コピーが配列に配置されます。
    • ソースがヒープ確保されており、デスティネーションがアリーナ確保されている場合:コピーは作成されません。
    • ソースがアリーナ確保されており、デスティネーションがヒープ確保されている場合:コピーが作成され、配列に配置されます。
    • ソースとデスティネーションの両方がヒープ確保されている場合:オブジェクトポインタは直接基になる配列に追加されます。

    これは、リピートフィールドによって指されるすべてのオブジェクトが、リピートフィールドのアリーナポインタによって示される同じ所有権ドメイン(ヒープまたは特定のアリーナ)にあるという不変条件を維持します。

  • SubMessageType* ReleaseLast(): リピートフィールドの最後のメッセージに相当するヒープ確保されたメッセージを返し、それをリピートフィールドから削除します。リピートフィールド自体がNULLのアリーナポインタを持つ場合(したがって、それが指すメッセージはすべてヒープ確保されている場合)、このメソッドは単に元のオブジェクトへのポインタを返します。それ以外の場合、リピートフィールドが非NULLのアリーナポインタを持つ場合、このメソッドはヒープ確保されたコピーを作成し、そのコピーを返します。どちらの場合も、呼び出し元はヒープ確保されたオブジェクトの所有権を受け取り、オブジェクトの削除に責任を持ちます。

  • void UnsafeArenaAddAllocated(SubMessageType* value): AddAllocated()に似ていますが、ヒープ/アリーナのチェックやメッセージのコピーを行いません。提供されたポインタをこのリピートフィールドの内部ポインタ配列に直接追加します。安全な使用方法の詳細については、allocated/releaseパターンを参照してください。

  • SubMessageType* UnsafeArenaReleaseLast(): ReleaseLast()に似ていますが、リピートフィールドが非NULLのアリーナポインタを持っていてもコピーを行いません。代わりに、リピートフィールドにあったオブジェクトへのポインタを直接返します。安全な使用方法の詳細については、allocated/releaseパターンを参照してください。

  • void ExtractSubrange(int start, int num, SubMessageType** elements): リピートフィールドからインデックスstartから始まるnum個の要素を削除し、elementsがNULLでなければそれらを返します。リピートフィールドがアリーナ上にある場合、要素が返される際には、要素はまずヒープにコピーされます。どちらの場合(アリーナありまたはなし)でも、呼び出し元は返されたオブジェクトをヒープ上で所有します。

  • void UnsafeArenaExtractSubrange(int start, int num, SubMessageType** elements): リピートフィールドからインデックスstartから始まるnum個の要素を削除し、elementsがNULLでなければそれらを返します。ExtractSubrange()とは異なり、このメソッドは抽出された要素を決してコピーしません。安全な使用方法の詳細については、allocated/releaseパターンを参照してください。

リピート文字列フィールド

文字列のリピートフィールドは、メッセージのリピートフィールドと同じ新しいメソッドと変更されたセマンティクスを持ちます。なぜなら、それらも基になるオブジェクト(つまり、文字列)をポインタ参照によって維持するからです。

使用パターンとベストプラクティス

アリーナ確保されたメッセージを使用する場合、いくつかの使用パターンが意図しないコピーやその他のパフォーマンスへの悪影響を引き起こす可能性があります。アリーナ用にコードを適合させる際に変更が必要になる可能性のある、以下の一般的なパターンに注意してください。(なお、API設計では正しい動作が依然として保証されるように注意を払っていますが、より高性能なソリューションにはいくつかの手直しが必要になる場合があります。)

意図しないコピー

arena allocationを使用しない場合はオブジェクトのコピーを決して作成しないいくつかのメソッドが、アリーナサポートが有効になるとコピーを作成することがあります。これらの不要なコピーは、オブジェクトが適切に確保されていることを確認したり、以下で詳しく説明するように、提供されているアリーナ固有のメソッドバージョンを使用したりすることで回避できます。

Set Allocated/Add Allocated/Release

デフォルトでは、release_field()およびset_allocated_field()メソッド(単数メッセージフィールド用)、およびReleaseLast()およびAddAllocated()メソッド(リピートメッセージフィールド用)は、ユーザーコードがサブメッセージを直接アタッチおよびデタッチし、データをコピーせずにポインタの所有権を渡すことを可能にします。

しかし、親メッセージがアリーナ上にある場合、これらのメソッドは既存の所有権契約との互換性を維持するために、渡されたり返されたりするオブジェクトをコピーする必要がある場合があります。より具体的には、所有権を取得するメソッド(set_allocated_field()およびAddAllocated())は、親がアリーナ上にあり、新しいサブオブジェクトがそうでない場合、またはその逆、またはそれらが異なるアリーナ上にある場合にデータをコピーすることがあります。所有権を解放するメソッド(release_field()およびReleaseLast())は、親がアリーナ上にある場合、契約により返されるオブジェクトがヒープ上になければならないため、データをコピーすることがあります。

このようなコピーを避けるために、コピーが**決して実行されない**対応する「unsafe arena」バージョンのメソッドを追加しました:単数およびリピートフィールド用にそれぞれunsafe_arena_set_allocated_field()unsafe_arena_release_field()UnsafeArenaAddAllocated()、およびUnsafeArenaRelease()です。これらのメソッドは、安全に使用できるとわかっている場合にのみ使用してください。これらのメソッドには2つの一般的なパターンがあります。

  • 同じアリーナの異なる部分間でメッセージツリーを移動する。この場合、安全であるためにはメッセージが同じアリーナ上になければならないことに注意してください。
  • コピーを避けるために、所有されているメッセージを一時的にツリーに貸し出す。unsafeな*add*/*set*メソッドとunsafeな*release*メソッドをペアにすることで、どちらのメッセージがどのように所有されているかに関わらず、最も安価な方法で貸し出しを行います(このパターンは、同じアリーナ上、異なるアリーナ上、またはアリーナなしの場合でも機能します)。unsafeな*add*/*set*とその対応する*release*の間、借用者はスワップ、移動、クリア、または破棄されてはならず、貸し出されたメッセージはスワップまたは移動されてはならず、貸し出されたメッセージは借用者によってクリアまたは解放されてはならず、貸し出されたメッセージは破棄されてはならないことに注意してください。

これらのメソッドを使用して不要なコピーを避ける方法の例を以下に示します。アリーナ上に次のメッセージを作成したとします。

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

代わりに「unsafe arena」バージョンを使用すると、コピーが回避されます。

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

これらのメソッドの詳細については、上記の埋め込みメッセージフィールドセクションを参照してください。

Swap

2つのメッセージの内容がSwap()で交換されるとき、2つのメッセージが異なるアリーナ上にある場合、または片方がアリーナ上でもう片方がヒープ上にある場合、基になるサブオブジェクトがコピーされることがあります。このコピーを避けたい場合で、(i) 2つのメッセージが同じアリーナ上にあるか、異なるアリーナ上でもアリーナのライフタイムが同等であることを知っているか、(ii) 2つのメッセージがヒープ上にあることを知っている場合は、新しいメソッドUnsafeArenaSwap()を使用できます。このメソッドは、アリーナチェックのオーバーヘッドを避け、コピーが発生した場合でもそのコピーを避けることができます。

例えば、以下のコードは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_2message_1と同じアリーナ上に確保します。

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

粒度

ほとんどのアプリケーションサーバーのユースケースでは、「リクエストごとにアリーナ」モデルがうまく機能することがわかっています。ヒープのオーバーヘッドを削減するため(より小さなアリーナをより頻繁に破棄する)、または認識されるスレッド競合の問題を減らすために、アリーナの使用をさらに細かく分割したくなるかもしれません。しかし、より細かい粒度のアリーナを使用すると、上記で説明したように、意図しないメッセージのコピーが発生する可能性があります。また、マルチスレッドのユースケース向けにArenaの実装を最適化することにも力を入れているため、複数のスレッドがそのリクエストを処理する場合でも、リクエストのライフタイム全体で単一のアリーナを使用することが適切であるはずです。

以下は、arena allocation APIのいくつかの機能を示す簡単な完全な例です。

// my_feature.proto
edition = "2023";

import "nested_message.proto";

package feature_package;

// NEXT Tag to use: 4
message MyFeatureMessage {
  string feature_name = 1;
  repeated int32 feature_data = 2;
  NestedMessage nested_message = 3;
};
// nested_message.proto
edition = "2023";

package feature_package;

// NEXT Tag to use: 2
message NestedMessage {
  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("Editions 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. 現在、文字列フィールドは、含まれるメッセージがアリーナ上にある場合でも、データをヒープ上に格納します。未知のフィールドもヒープ確保されます。 ↩︎

  2. 「完全に互換性のある」型であるための要件はprotobufライブラリの内部的なものであり、信頼できると仮定すべきではありません。 ↩︎