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サイクルの後、すべてのオブジェクトを指すことができます。メモリに配置します。