管理された言語でパーティクルプールを使用する価値はありますか?


10

パーティクルシステムのオブジェクトプールをJavaで実装するつもりでしたが、ウィキペディアでこれを見つけました。言い換えると、JavaやC#などのマネージ言語ではオブジェクトプールを使用する価値がないとあります。これは、C ++などの非マネージ言語では数百の操作に比べて、割り当てには数十の操作しか必要ないためです。

しかし、誰もが知っているように、すべての命令はゲームのパフォーマンスを損なう可能性があります。たとえば、MMO内のクライアントのプール:クライアントがプールに入ったり出たりする速度が速すぎません。しかし、粒子は一秒間に数十回更新される可能性があります。

問題は、マネージ言語でパーティクル(具体的には、死んですぐに再作成されるもの)のオブジェクトプールを使用する価値があるかどうかです。

回答:


14

はい、そうです。

割り当て時間だけが要因ではありません。割り当てには、ガベージコレクションパスの誘導などの副作用があり、パフォーマンスに悪影響を及ぼすだけでなく、パフォーマンスに予期しない影響を与える可能性があります。これの詳細は、言語とプラットフォームの選択によって異なります。

また、プーリングは、一般に、プール内のオブジェクトの参照の局所性を向上させます。たとえば、オブジェクトをすべて隣接する配列に保持することにより、オブジェクトの局所性が向上します。これにより、プールのコンテンツ(または少なくともそのライブ部分)を反復しながらパフォーマンスを向上させることができます。これは、反復の次のオブジェクトがすでにデータキャッシュにある傾向があるためです。

最も内側のゲームループで割り当てを回避しようとする従来の知識は、マネージ言語(特に、たとえばXNAを使用する場合の360など)にも適用されます。その理由はわずかに異なります。


+1ただし、構造体を使用する価値があるかどうかは触れませんでした。基本的にはそうではありません(値の型をプールしても何も達成されません)。
ジョナサンディキンソン

2
OPはJavaの使用について言及しているので、構造体には触れませんでした。その言語での値の型/構造の動作については、あまり詳しくありません。

Javaには構造体はなく、クラスのみ(常にヒープ上)があります。
ブレンダンロング

1

Javaの場合、オブジェクトをプールすることはあまり役に立ちません*まだ残っているオブジェクトの最初のGCサイクルはそれらをメモリ内で再シャッフルし、それらを「エデン」空間から移動させ、潜在的にプロセスの空間的局所性を失います。

  • どのような言語でも、スレッドのような破壊と作成に非常にコストがかかる複雑なリソースをプールすることは常に役立ちます。それらの作成と破棄の費用は、リソースへのオブジェクトハンドルに関連付けられたメモリとはほとんど関係がないため、これらはプールする価値があります。ただし、パーティクルはこのカテゴリに適合しません。

Javaは、オブジェクトをEdenスペースにすばやく割り当てるときに、シーケンシャルアロケーターを使用した高速バースト割り当てを提供します。この順次割り当て方式はmalloc、Cの場合よりも高速で高速です。これは、直接順次方式で割り当てられたメモリをプールするだけなので、メモリの個々のチャンクを解放できないという欠点があります。これは、たとえば、データ構造から何も削除する必要がないデータ構造に非常に高速に割り当て、すべてを追加してそれを使用し、後で全体を捨てる場合に、Cで役立つトリックです。

個々のオブジェクトを解放できないという欠点があるため、Java GCは、最初のサイクルの後に、メモリの割り当てを可能にする低速でより汎用的なメモリアロケータを使用して、Edenスペースから割り当てられたすべてのメモリを新しいメモリ領域にコピーします。別のスレッドの個々のチャンクで解放されます。次に、現在コピーされてメモリ内の他の場所に存在する個々のオブジェクトを気にすることなく、全体としてエデン空間に割り当てられたメモリを破棄できます。最初のGCサイクルの後、オブジェクトがメモリで断片化する可能性があります。

最初のGCサイクルの後でオブジェクトが断片化する可能性があるため、主にメモリアクセスパターン(参照の局所性)を改善し、割り当て/割り当て解除のオーバーヘッドを削減するためのオブジェクトプーリングの利点は、ほとんど失われます...通常、常に新しいパーティクルを割り当て、Edenスペースでまだ新鮮であり、「古く」なり、メモリに分散する前にそれらを使用することで、参照の局所性が向上します。ただし、(JavaでCに匹敵するパフォーマンスを得るなど)非常に役立つ可能性があるのは、パーティクルにオブジェクトを使用しないようにし、プレーンな古いプリミティブデータをプールすることです。簡単な例として、次の代わりに:

class Particle
{
    public float x;
    public float y;
    public boolean alive;
}

次のようなことをしてください:

class Particles
{
    // X positions of all particles. Resize on demand using
    // 'java.util.Arrays.copyOf'. We do not use an ArrayList
    // since we want to work directly with contiguously arranged
    // primitive types for optimal memory access patterns instead 
    // of objects managed by GC.
    public float x[];

    // Y positions of all particles.
    public float y[];

    // Alive/dead status of all particles.
    public bool alive[];
}

既存のパーティクルのメモリを再利用するために、これを行うことができます:

class Particles
{
    // X positions of all particles.
    public float x[];

    // Y positions of all particles.
    public float y[];

    // Alive/dead status of all particles.
    public bool alive[];

    // Next free position of all particles.
    public int next_free[];

    // Index to first free particle available to reclaim
    // for insertion. A value of -1 means the list is empty.
    public int first_free;
}

今際nthの粒子ダイは、それが再利用できるようにするために、そのような無料のリストにそれをプッシュします:

alive[n] = false;
next_free[n] = first_free;
first_free = n;

新しいパーティクルを追加するときに、フリーリストからインデックスをポップできるかどうかを確認します。

if (first_free != -1)
{
     int index = first_free;

     // Pop the particle from the free list.
     first_free = next_free[first_free];

     // Overwrite the particle data:
     x[index] = px;
     y[index] = py;
     alive[index] = true;
     next_free[index] = -1;
}
else
{
     // If there are no particles in the free list
     // to overwrite, add new particle data to the arrays,
     // resizing them if needed.
}

これは最も快適なコードではありませんが、これを使用すると、すべてのパーティクルデータが常に連続して保存されるため、シーケンシャルパーティクル処理が常に非常にキャッシュフレンドリーな非常に高速なパーティクルシミュレーションを実行できるはずです。このタイプのSoA担当者は、リフレクション/動的ディスパッチのオブジェクトメタデータであるパディングを心配する必要がないため、メモリ使用量も削減し、ホットフィールドをコールドフィールドから分離します(たとえば、必ずしもデータに関心があるわけではありません)物理パス中のパーティクルの色のようなフィールドなので、キャッシュラインにロードしてそれを使用せずに排除するのは無駄です。

コードを扱いやすくするために、浮動小数点数の配列、整数の配列、およびブール値の配列を格納する、独自の基本的なサイズ変更可能なコンテナーを作成する価値があるかもしれません。繰り返しますが、ジェネリックスとArrayListここでは(少なくとも最後にチェックしたときから)、連続したプリミティブデータではなく、GCで管理されたオブジェクトが必要になるため、ここでは使用できません。Edenスペースを離れた後、必ずしも連続してintいないGC管理の配列など、の連続した配列を使用したいと考えていますInteger

プリミティブ型の配列を使用すると、それらは常に隣接していること保証されているため、非常に望ましい参照の局所性(逐次的なパーティクル処理の場合、違いが生まれます)と、オブジェクトプーリングが提供するすべての利点を得ることができます。オブジェクトの配列の場合、それは代わりに、すべてのオブジェクトを一度にEdenスペースに割り当てたと仮定して、連続的な方法でオブジェクトを指すようになるポインターの配列に多少似ていますが、GCサイクルの後、すべてのオブジェクトを指すことができます。メモリに配置します。


1
これはこの問題についての素晴らしい記事であり、5年間のJavaコーディングの後で、私はそれをはっきりと見ることができます。Java GCは馬鹿げているわけではなく、ゲームプログラミング用に作成されたものでもないため(データの局所性などは実際には気にしないので)、P
Gustavo Maciel
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.