3. メモリ管理

3.1. メモリアロケーターの差し替え[推奨]

NEXライブラリは内部で動的なメモリ確保を頻繁に行います。 MemoryManager::SetBasicMemoryFunctions() でアプリケーションで用意したメモリアロケーターを設定します。 メモリアロケーターを設定しない場合、NEXライブラリは標準のmalloc()/free()関数を利用してメモリ確保を行います。 製品ではアプリケーション独自のメモリ管理機構を使うことを推奨します。

メモリアロケーターを設定した場合、NEX内部で動的にメモリを確保するときと、 RootObject を継承した公開クラスをアプリケーションが new するときに設定されたメモリアロケーターが利用されます。( RootObject を継承していない場合には、クラス説明に特記されています。 )

3.2. メモリ枯渇時への備え[必須]

NEXライブラリ内部では、動的メモリ確保は必ず成功することを前提としたコードが記述されています。 そのため、必ず有効なメモリへのアドレスを返すように実装してください。 具体的には、以下のような実装をしていただく必要があります。

  • 予定してあるメモリ量を超えてメモリを確保したらライブラリをシャットダウンする
  • 事前に数KB程度(正常にライブラリシャットダウンまで実行できるであろう量)のメモリを確保しておき、確保できなくなったらその数KBのメモリを解放する。その後ライブラリをシャットダウンする
  • 予めメモリに十分余裕を持っておく(通信環境が悪くなった場合などでもメモリが足りなくなることがないように十分なデバッグを行なってください)

3.3. パケットバッファマネージャーの設定[推奨]

NEXライブラリ内部の動的メモリ確保のうち、 P2P 通信のパケットバッファのメモリ管理がパケットバッファマネージャーで行われます。 アプリケーションの通信データの特性にあわせて、パケットバッファマネージャーの設定を行ってください。 詳細は 3.8. を参照してください。

3.4. スレッドセーフの考慮

NEXのAPIを複数のスレッドから使用する場合、メモリ確保も複数スレッドから発生するので、アロケーターをスレッドセーフにする必要があります。 詳細は 4.1. を参照してください。

3.5. アプリケーションからNEXのアロケーターを使用する方法

RootObjectを継承したクラスに対してnew, もしくはqNewを呼び出すとMemoryManager::SetBasicMemoryFunctionsで指定したアロケーターが利用されます。

Code 3.1 NEXのアロケーター利用例
// NATTraversalClient は RootObject を継承したクラスなので、
// 以下のコードで MemoryManager::SetBasicMemoryFunctions で設定したアロケーターが使用される
NATTraversalClient* pNATTraversalClient = qNew NATTraversalClient();
qDelete pNATTraversalClient;

3.6. アロケーターの高速化

CTRでNEXを利用した際にCPUが高負荷になる原因の1つとして、NEXがメモリ確保・開放を非常に高頻度で行うということが挙げられます。

そこで、メモリ確保・開放の負荷を低減する方法としてマルチユニットヒープの採用を推奨します。 マルチユニットヒープは複数のユニットヒープを持ち、要求されたアロケーションサイズを確保可能な最適なユニットヒープからメモリを確保する、高速なメモリアロケーターです。 ユニットヒープを使う分、通常のメモリアロケーターよりはメモリを無駄に消費するのでご注意ください。

NEXのサンプルプログラムとしてマルチユニットヒープを公開しています。 このサンプルのマルチユニットヒープを改変して、 アプリケーションに組み込んでいただいても構いません。 詳細な実装についてはサンプルを参照してください。

3.7. サーバー機能使用時のメモリ使用量目安

NEX サーバー機能使用時に必要なメモリ量の目安は使用する機能や呼び出し頻度などによって異なりますが、トランスポートスレッドモードでは以下のようになります。

  • NgsFacade::Login() 非同期処理中 : 最大約 260 KB
  • ログイン中に非同期処理を走らせずに Scheduler::Dispatch のみ呼び続けているとき(定常時) : 約 214KB
  • マッチメイク: 非同期処理を同時に複数呼ばない前提で 定常時に必要なメモリ + 64KB 程度あれば問題なく動作します。
  • ランキング: ほぼ RankingClient::GetRanking() で取得する量に依存します。20 件取得時に 34KB, 最大 255 件取得時 472 KB 程度を、定常時に使用するメモリに加えて使用します。

これらのメモリに加え、緊急用に 2 割程度余分にメモリを確保し、想定よりも多くのメモリが消費されたときに速やかにログアウト処理を行うことを推奨します。

3.8. パケットバッファのメモリ管理

NEXのメモリ確保のうち、 リアルタイム通信で使用するパケットバッファのメモリ管理はパケットバッファマネージャーによって行われます。

パケットバッファマネージャーは、 リアルタイム通信開始前に専用のメモリプール(パケットバッファプール)を一括で確保し、パケットバッファのメモリ確保要求に応じてパケットバッファプールからメモリ割り当てを行う機能です。

パケットバッファプールを使用したパケットバッファのメモリ管理はデフォルトで有効となります。

パケットバッファプール自体のメモリ確保は他のNEXオブジェクトと同じ方法で確保されます。

パケットバッファプールはあらかじめ制限されたサイズで確保されるため、大量のパケット送受信が発生した際のメモリ消費によるNEX全体のメモリ枯渇を防ぐことができます。

以下で使用されるパケットバッファについてはパケットバッファマネージャーでは管理せず、他のNEXオブジェクトと同じメモリ管理が行われます。

  • サーバーとの通信
  • NEX内部で自動的に行われる複製オブジェクトを使用した通信
  • 複製オブジェクトのうちコアオブジェクトを使用した通信
  • パケットバッファマネージャーで確保可能なメモリブロックサイズの最大(1364byte)を超えるデータの送受信

複製オブジェクトについては 12. を参照してください。

注意

送信サイズが大きいデータはパケットバッファマネージャーの管理外となるので、 NEX全体のメモリ使用量への影響に注意してください。

3.8.1. パケットバッファプールの無効化

パケットバッファプールを使用せず、パケットバッファのメモリ管理を他のNEXオブジェクトと同じメモリ管理にするには、PacketBufferManager::EnablePacketBufferPool()でfalseを設定します。

リアルタイム通信開始前に設定してください。

NEX初期化毎に設定する必要があります。設定した値はNEX終了時まで保持されます。

Code 3.2 パケットバッファのメモリ管理を、他のNEXオブジェクトと同じメモリ管理で行う例
qBool ret = PacketBufferManager::EnablePacketBufferPool(false);
if (!ret)
{
    // P2P 通信中は設定できずエラーになります。
}

3.8.2. パケットバッファプール

パケットバッファプールは リアルタイム 通信向けのパケットバッファのメモリ割り当て用に確保されるメモリプールです。

パケットバッファプールの確保は リアルタイム 通信開始前に確保され、 リアルタイム 通信停止で解放されます。 具体的には、 ConnectivityManager::StartNATSession()、 NetZ::NetZ()、 VSocket::Open()の呼び出しを契機に確保され、ConnectivityManager::StopNATSession()、 NetZ::~NetZ()、 VSocket::Close()の呼び出し契機で解放されます。

パケットバッファプールは、複数のユニットヒープを使用して管理され、パケットのペイロード以外にペイロードに付随するパケット管理に使用するオブジェクトの確保に使用されます。 このうち、ペイロード用のユニットヒープについて、ユニットサイズやメモリブロック数をアプリケーション側でカスタマイズすることができます。アプリケーションの通信データサイズにあわせてカスタマイズしてください。

パケットバッファの確保は、ユニットサイズが小さいユニットヒープから順にメモリ確保を試み、最初に確保可能となったユニットヒープが使用されます。 最大サイズのユニットヒープでも確保できない場合は、パケットバッファプールが枯渇した状態となります(1364byteを超えるサイズのバッファはパケットバッファプールでの確保対象外です)。

デフォルトのパケットバッファプールは、ペイロード用に1364byteのユニットサイズのユニットヒープが使用され、パケット管理オブジェクト用のユニットヒープを含めて300kbyteの領域を使用します。

3.8.3. ユニットヒープ構成のカスタマイズ

PacketBufferManager::SetUnitHeapParam()を使用することで、ペイロード用のユニットヒープ構成を設定することができます。 パケット管理オブジェクト用のユニットヒープは、ペイロード用のユニットヒープ構成にあわせてNEX内部で自動設定されます。

ユニットヒープ構成の設定は P2P 通信開始前に行ってください。 P2P 通信中は設定できません。 一度設定したユニットヒープ構成はNEX終了時まで保持されますので、 P2P セッション毎に設定し直す必要はありません。

ユニットヒープ構成は、以下の条件を満たす必要があります。

  • ユニットサイズは4の倍数かつ1364byte以下
  • 最大サイズ(1364)のユニットサイズのユニットヒープを持つこと

最大サイズのユニットヒープは、送信効率を上げるために行われるパケットのバンドリングにおける送受信やNEX内の通信などに使用されるために必要となります。 また、メモリブロック数は64以上になるようにしてください。不足する場合はさらに増やしてください。

注意

  • ユニットサイズはNEX内部で付与されるヘッダなどを考慮してアプリケーションでの送信サイズより80byte以上大きくし、4の倍数になるように設定してください。
  • 不正なユニットヒープ構成を設定しようとするとPacketBufferManager::SetUnitHeapParam()はアサートエラーになります。
Code 3.3 ユニットヒープ構成の設定例
qVector<PacketBufferManager::UnitHeapParam> paramList(3);
// ショートサイズのパケット用
paramList[0].unitSize = 128+80; // +80はNEXで付与されるヘッダなどを考慮
paramList[0].unitNum = 32;
// アプリケーションで主に使用するサイズ+64
paramList[1].unitSize = 500+80; // +80はNEXで付与されるヘッダなどを考慮
paramList[1].unitNum = 64;
// パケットバンドル用のパケットサイズ(最大の1364を使用)
paramList[2].unitSize = 1364;
paramList[2].unitNum = 128; // 多く取れるようにします
qBool ret = PacketBufferManager::SetUnitHeapParam(paramList);
if (!ret)
{
    // P2P 通信中は設定できずエラーになります。
    // ユニットヒープ構成が不正であれば、アサートエラーになります。
}

上記例のようにPacketBufferManager::UnitHeapParamを個別に設定して構成決定をする以外に、PacketBufferManager::GetSimpleUnitHeapParam()を使用して簡易構成を取得することができます。

PacketBufferManager::GetSimpleUnitHeapParam()は、パケットバッファプールのサイズと平均送信データサイズを指定します。 簡易構成は、ペイロード用とパケット管理オブジェクト用のユニットヒープのメモリ合計が指定したパケットバッファプールのサイズに収まるように算出されます。 また、ペイロード用のユニットヒープで使用するメモリのうち、半分が平均送信データサイズ、残り半分が最大サイズ(1364)のユニットヒープに割り当てられます。 なお、ユニットサイズはNEX内部で付与されるヘッダなどを考慮して平均送信データ送信サイズを80増やして4の倍数に補正された値となります。

Code 3.4 簡易構成の設定例
// プールサイズが500kbyte、平均送信データサイズが500byte
qVector<PacketBufferManager::UnitHeapParam> param;
qBool ret = PacketBufferManager::GetSimpleUnitHeapParam(500*1024, 500, param);
if (!ret)
{
    // プールサイズが小さい場合や平均送信データサイズが不正の場合、エラーになります。
}
else
{
    ret = PacketBufferManager::SetUnitHeapParam(param);
    if (!ret)
    {
        // P2P 通信中は設定できずエラーになります。
        // ユニットヒープ構成が不正であれば、アサートエラーになります。
    }
}

3.8.4. 統計情報の取得

PacketBufferManager::GetUnitHeapDebugInfo()を使用することで、ペイロード用ユニットヒープのメモリ確保の統計情報を取得することができます。 使用メモリブロック数やメモリ確保失敗数などを取得することができ、ユニットヒープの構成決定のヒントや枯渇状態のチェックに使用できます。

NEX内部ではバッファ確保前にあらかじめ必要数分の空きをチェックし、不足する場合にメモリ枯渇発生回数としてカウントする場合があります。 そのため、実際の確保数のピーク値が最大数に達しない状態でメモリ枯渇発生回数が増加することがあります。

注意

最もユニットサイズの大きいユニットヒープのメモリ枯渇発生回数が増加する場合は、メモリブロック数の最大数を上げるようにユニットヒープ構成を設定してください。

Code 3.5 パケットバッファの枯渇状態を見る例
qVector<PacketBufferManager::UnitHeapDebugInfo> debugInfo;
PacketBufferManager::GetUnitHeapDebguInfo(debugInfo);
qVector<PacketBufferManager::UnitHeapDebugInfo>::const_iterator it;
it = debugInfo.begin();
while (it != debugInfo.end())
{
    // 各ユニットヒープのユニットサイズとメモリ枯渇発生回数の表示
    QLOG(EventLog::Info, NEX_T("unitSize:") << it->unitSize << NEX_T(" outOfMemory:") << it->outOfMemory);
}

3.8.5. パケットバッファプール枯渇時の動作

パケットバッファプールが枯渇した場合、 以下の状態を引き起こします。

  • 他ステーションからの受信パケットがドロップされる
  • RMCやダイレクトストリームなどのデータ送信を行うAPIの呼び出しが失敗する

他ステーションからの受信パケットがドロップされることで、ステーション間のNATトラバーサルが失敗したり、キープアライブタイムアウトが発生してステーション間の接続が切断される要因となります。

送信APIがパケットバッファが不足してエラーを返す場合、QERROR(Transport, PacketBufferFull)を返します。 エラーが返った場合は時間をおいて必要に応じて再送信してください。

高頻度でパケットバッファが不足する場合はパケットバッファプールに割り当てるメモリを増やすか、Reliable通信の通信頻度を下げたり、Reliable通信の最大送信バッファ数を減らしてください。

Reliable最大送信バッファ数の設定については以下を参照してください。

3.9. P2P 通信中の動的メモリおよびパケットバッファ使用量目安

3.9.1. 動的メモリおよびパケットバッファ使用量の最大値目安

通信環境や送信、受信頻度によってNEXで必要とされる最大メモリ量やパケットバッファ使用量は大きく変わってきます。 以下のデータはあくまでも目安としてお考えください。

Table 3.1 のマッチメイク条件で、 パケット遅延・ロスエミュレータを使って通信環境を変化させたときの動的メモリおよびパケットバッファ使用量を Table 3.2 に示します。

動的メモリ使用量は、32byteアライメントのメモリアロケーターを使い、以下の期間の動的メモリ及びパケットバッファ使用量のピークを計測しました。

  1. NEX 初期化
  2. マッチメイク開始
  3. マッチメイク終了
  4. NEX 破棄

パケットバッファの使用量は、580byteのメモリブロック80個、1364byteのメモリブロック64個の使用数を表しています。

Table 3.1 メモリ使用量調査のマッチメイク条件
項目 設定値 備考
スレッドモード Core::ThreadModeUnsafeTransportBuffer  
マッチメイク参加台数 4台  
マッチメイク時間 3分 全員がセッションに参加してから抜けるまでの時間です。
リライアブル通信の頻度 12回/sec 1回の送信で全員に500byteのデータを送ります。(DirectStreamを使用)
アンリライアブル通信の頻度 60回/sec 1回の送信で全員に500byteのデータを送ります。(DirectStreamを使用)
パケットバッファプールのサイズ 200kbyte 580byteを80個、1364byteを64個のメモリブロックを使用するユニットヒープ構成を使用
オートスタックの使用 使用していません Core::UseThreadAutoStack(false)を設定。
Table 3.2 デベロップビルドでのメモリ使用量の目安
パケット遅延、ロスの環境 最大メモリ使用量 パケットバッファ使用量 備考
パケット遅延、パケットロスがほぼない 良好 な環境 約800kbyte 25/80, 15/64  
パケット遅延200msec、パケットロス 20% の 劣悪 な環境 約800kbyte 80/80, 60/64 マッチメイクの失敗、ゲームサーバーとの通信断、送信エラーが発生するようになります。場合によってはパケットバッファが不足します。
パケット遅延200msec、パケットロス 20%超え 約650kbyte 2/80, 2/64 ロス率が高く P2P セッションを張るまでに至らない場合の使用量です。
Table 3.3 リリースビルドでのメモリ使用量の目安
パケット遅延、ロスの環境 最大メモリ使用量 パケットバッファ使用量 備考
パケット遅延、パケットロスがほぼない 良好 な環境 約780kbyte 25/80, 15/64  
パケット遅延200msec、パケットロス 20% の 劣悪 な環境 約780kbyte 80/80, 60/64 マッチメイクの失敗、ゲームサーバーとの通信断、送信エラーが発生するようになります。場合によってはパケットバッファが不足します。
パケット遅延200msec、パケットロス 20%超え 約620kbyte 2/80, 2/64 ロス率が高く P2P セッションを張るまでに至らない場合の使用量です。

デベロップビルドの例の場合、

上記の劣悪な環境(パケット遅延200msec、パケットロス20%)でも アプリケーションを動作させるためには、 800kbyte 以上のメモリが必要ということになります。

メモリ枯渇の対策例としては、 少し余裕をみて 900kbyte のメモリを用意し、メモリ使用量が 850kbyte になったらライブラリをシャットダウンするといった対策をすると良いでしょう。

パケットバッファの観点においては、劣悪な環境(パケット遅延200msec、パケットロス20%)では パケットバッファが不足する確率が高くなるので、メモリの余力がある場合は更に数十~100kbyte程度パケットバッファプールを増やすことで パケットバッファが不足する確率を低くすることができます。

3.9.2. 動的メモリ確保サイズの分布

通信環境や送信、受信頻度によってNEXで必要とされる最大メモリ量は大きく変わってきます。以下のデータはあくまでも目安としてお考えください。 なお、パケットバッファプールを利用することで、通信環境の違いによるP2Pデータ通信のメモリ使用量の増減を抑えることができます。

NEXが要求するメモリ確保サイズはある程度特定のサイズに偏っています。 頻繁に確保されるサイズのメモリ要求を、ユニットヒープ等の高速なメモリアロケーターに差し替えることでNEXのメモリ確保に必要なCPU負荷低減が可能です。

補足

ユニットヒープは通常のヒープと比べてメモリを無駄に消費します。 ユニットヒープを使用する場合でも、ある程度大きなサイズのメモリ確保(4096byte程度以上)は通常のヒープを使用するといった方法を推奨します。

Table 3.4 のマッチメイク条件で、 パケットロス、パケット遅延ともにほぼない良好なネットワーク環境でのメモリ確保サイズの分布の目安を Table 3.5 に示します。 メモリ確保の高速化の参考にしてください。

動的メモリ使用量は、32byteアライメントのメモリアロケーターを使い、以下の期間のメモリ確保サイズの分布を計測しました。

  1. NEX 初期化
  2. マッチメイク開始
  3. マッチメイク終了
  4. NEX 破棄
Table 3.4 動的メモリ確保サイズ調査のマッチメイク条件
項目 設定値 備考
スレッドモード Core::ThreadModeUnsafeTransportBuffer  
マッチメイク参加台数 4台  
マッチメイク時間 3分 全員がセッションに参加してから抜けるまでの時間です。
リライアブル通信の頻度 12回/sec 1回の送信で全員に500byteのデータを送ります。(DirectStreamを使用)
アンリライアブル通信の頻度 60回/sec 1回の送信で全員に500byteのデータを送ります。(DirectStreamを使用)
パケットバッファプールのサイズ 200kbyte 580byte 80個、1364byte 64個のメモリブロックを使用するユニットヒープ構成を使用
オートスタックの使用 使用していません Core::UseThreadAutoStack(false)を設定。
Table 3.5 デベロップビルドでの動的メモリ要求サイズの分布の目安
要求サイズ 同時確保最大数の目安 合計確保回数の目安 備考
1[byte] ~ 32[byte] 820 96000  
33[byte] ~ 64[byte] 540 53000  
65[byte] ~ 128[byte] 120 1400  
129[byte] ~ 256[byte] 90 400  
257[byte] ~ 512[byte] 70 20000  
513[byte] ~ 1024[byte] 180 11000  
1025[byte] ~ 1536[byte] 160 50000  
1537[byte] ~ 2048[byte] 6 8  
2049[byte] ~ 4096[byte] 17 100  
4097[byte] ~ 8192[byte] 4 4  
32767[byte] 3 3 Core::ThreadModeUnsafeTransportBufferの場合スレッドスタックとして32Kbyte - 1byte が最大3つ確保される
200[kbyte] 1 1 パケットバッファプールで使用
Table 3.6 リリースビルドでの動的メモリ要求サイズの分布の目安
要求サイズ 同時確保最大数の目安 合計確保回数の目安 備考
1[byte] ~ 32[byte] 820 96000  
33[byte] ~ 64[byte] 540 52000  
65[byte] ~ 128[byte] 120 1300  
129[byte] ~ 256[byte] 70 400  
257[byte] ~ 512[byte] 60 20000  
513[byte] ~ 1024[byte] 180 11000  
1025[byte] ~ 1536[byte] 160 50000  
1537[byte] ~ 2048[byte] 6 8  
2049[byte] ~ 4096[byte] 17 100  
4097[byte] ~ 8192[byte] 4 4  
32767[byte] 3 3 Core::ThreadModeUnsafeTransportBufferの場合スレッドスタックとして32Kbyte - 1byte が最大3つ確保される
200[kbyte] 1 1 パケットバッファプールで使用