C++ アリーナアロケーションガイド

アリーナアロケーションはC++専用の機能で、Protocol Buffersを扱う際のメモリ使用量を最適化し、パフォーマンスを向上させるのに役立ちます。

このページでは、アリーナアロケーションが有効な場合に、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::Create<MyMessage>(&arena);
  // ...
}

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

ほとんどの場合、アリーナアロケーションを使用しない場合と同じコードになります。

次のセクションでアリーナ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でない場合、返されるオブジェクトはアリーナ上に割り当てられ、その内部ストレージとサブタイプ(もしあれば)は同じアリーナ上に割り当てられ、そのライフタイムはアリーナのライフタイムと同じになります。オブジェクトを手動で削除/解放してはいけません。アリーナがライフタイムの目的でオブジェクトを所有します。

      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でない場合、このメソッドは型Tn個の要素のための生ストレージを割り当て、それを返します。アリーナは返されたメモリを所有し、自身が破棄される際に解放します。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(): このアリーナへのポインタを返します。直接的にはあまり有用ではありませんが、GetArena()メソッドが存在することを期待するテンプレートインスタンス化でArenaを使用できます。

スレッドセーフティ

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

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

アリーナアロケーションを有効にすると、以下のメッセージクラスメンバーが変更または追加されます。

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

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

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

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

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

これらのフィールド定義のいずれに対しても

optional Bar foo = 1;
required Bar foo = 1;

アリーナアロケーションが有効な場合、以下のメソッドが追加されるか、特別な動作をします。それ以外の場合、アクセサメソッドはデフォルトの動作を使用します。

  • 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と同じですが、親とサブメッセージの両方が同じアリーナ上にあることを前提としています。このバージョンのメソッドを使用すると、メッセージが特定のアリーナ上にあるかヒープ上にあるかをチェックする必要がないため、パフォーマンスが向上する可能性があります。これらを安全に使用する方法の詳細は、割り当て/解放パターンを参照してください。
  • Bar* unsafe_arena_release_foo(): release_foo()に似ていますが、すべての所有権チェックをスキップします。これらを安全に使用する方法の詳細は、割り当て/解放パターンを参照してください。

文字列フィールド

文字列フィールドは、親メッセージがアリーナ上にある場合でも、そのデータをヒープ上に保存します。このため、アリーナアロケーションが有効な場合でも、文字列アクセサメソッドはデフォルトの動作を使用します。

繰り返しフィールド

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

繰り返し数値フィールド

プリミティブ型を含むRepeatedFieldオブジェクトは、アリーナアロケーションが有効な場合、以下の新しい/変更されたメソッドを持ちます。

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

繰り返し埋め込みメッセージフィールド

メッセージを含むRepeatedPtrFieldオブジェクトは、アリーナアロケーションが有効な場合、以下の新しい/変更されたメソッドを持ちます。

  • 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()に似ていますが、ヒープ/アリーナのチェックやメッセージのコピーは行いません。提供されたポインタをこの繰り返しフィールドの内部ポインタ配列に直接追加します。これらを安全に使用する方法の詳細は、割り当て/解放パターンを参照してください。

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

  • 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()とは異なり、このメソッドは抽出された要素をコピーすることはありません。これらを安全に使用する方法の詳細は、割り当て/解放パターンを参照してください。

繰り返し文字列フィールド

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

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

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

意図しないコピー

アリーナアロケーションを使用しない場合、オブジェクトのコピーを作成しないメソッドでも、アリーナサポートが有効な場合はコピーが発生する可能性があります。これらの不要なコピーは、以下に詳述するように、オブジェクトが適切に割り当てられていること、および/または提供されているアリーナ固有のメソッドバージョンを使用することで回避できます。

割り当て/追加割り当て/解放を設定

デフォルトでは、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());

これらのメソッドの詳細については、上記の埋め込みメッセージフィールドセクションで確認できます。

スワップ

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の実装を最適化する努力も行っているため、単一のアリーナは、複数のスレッドがそのリクエストを処理する場合でも、リクエストライフタイム全体で適切に使用できるはずです。

以下は、アリーナアロケーション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. 現在、文字列フィールドは、含まれるメッセージがアリーナ上にある場合でも、そのデータをヒープ上に保存します。不明なフィールドもヒープ割り当てされます。 ↩︎

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