Para Java, não é tão útil agrupar objetos *, pois o primeiro ciclo de GC para objetos que ainda estão por aí os reorganizará na memória, movendo-os para fora do espaço "Eden" e potencialmente perdendo a localidade espacial no processo.
- É sempre útil em qualquer idioma reunir recursos complexos que são muito caros para destruir e criar threads semelhantes. Vale a pena agrupá-las porque as despesas de criação e destruição delas quase nada têm a ver com a memória associada ao identificador de objeto do recurso. No entanto, as partículas não se enquadram nessa categoria.
Java oferece alocação rápida de burst usando um alocador seqüencial quando você aloca objetos rapidamente no espaço Eden. Essa estratégia de alocação sequencial é super rápida, mais rápida do que malloc
em C, uma vez que apenas agrupa a memória já alocada de maneira sequencial direta, mas vem com a desvantagem de que você não pode liberar blocos individuais de memória. Também é um truque útil em C se você deseja alocar coisas super rápido para, por exemplo, uma estrutura de dados em que você não precise remover nada dela, basta adicionar tudo e depois usá-lo e jogar tudo fora mais tarde.
Devido a essa desvantagem de não conseguir liberar objetos individuais, o Java GC, após um primeiro ciclo, copiará toda a memória alocada do espaço Eden para novas regiões de memória usando um alocador de memória mais lento e de uso geral que permite que a memória seja armazenada. ser liberado em pedaços individuais em um encadeamento diferente. Depois, pode jogar fora a memória alocada no espaço do Éden como um todo, sem se preocupar com objetos individuais que agora foram copiados e vivem em outro lugar na memória. Após esse primeiro ciclo do GC, seus objetos podem acabar sendo fragmentados na memória.
Como os objetos podem acabar sendo fragmentados após o primeiro ciclo do GC, os benefícios do agrupamento de objetos, principalmente para melhorar os padrões de acesso à memória (localidade de referência) e reduzir a sobrecarga de alocação / desalocação, são amplamente perdidos ... para que você obtenha uma melhor localidade de referência normalmente alocando novas partículas o tempo todo e usá-las enquanto ainda estão frescas no espaço Eden e antes que se tornem "velhas" e potencialmente dispersas na memória. No entanto, o que pode ser extremamente útil (como obter o desempenho rival do C em Java) é evitar o uso de objetos para suas partículas e agrupar dados primitivos antigos simples. Para um exemplo simples, em vez de:
class Particle
{
public float x;
public float y;
public boolean alive;
}
Faça algo como:
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[];
}
Agora, para reutilizar a memória para partículas existentes, você pode fazer o seguinte:
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;
}
Agora quando o nth
partícula morrer, para permitir sua reutilização, empurre-a para a lista livre da seguinte maneira:
alive[n] = false;
next_free[n] = first_free;
first_free = n;
Ao adicionar uma nova partícula, veja se é possível exibir um índice da lista gratuita:
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.
}
Não é o código mais agradável de se trabalhar, mas com isso você poderá obter algumas simulações muito rápidas de partículas, pois o processamento seqüencial de partículas é muito amigável para o cache sempre, pois todos os dados de partículas sempre serão armazenados de forma contígua. Esse tipo de representante SoA também reduz o uso de memória, pois não precisamos nos preocupar com preenchimento, os metadados do objeto para reflexão / envio dinâmico e separa os campos quentes dos campos frios (por exemplo, não estamos necessariamente preocupados com dados campos como a cor de uma partícula durante a física passam, por isso seria um desperdício carregá-la em uma linha de cache apenas para não usá-la e despejá-la).
Para facilitar o trabalho do código, pode valer a pena escrever seus próprios contêineres redimensionáveis básicos que armazenam matrizes de flutuadores, matrizes de números inteiros e matrizes de booleanos. Novamente, você não pode usar genéricos e ArrayList
aqui (pelo menos desde a última vez que verifiquei), pois isso requer objetos gerenciados por GC, não dados primitivos contíguos. Queremos usar uma matriz contígua deint
, por exemplo, matrizes não gerenciadas por GC, Integer
que não serão necessariamente contíguas depois de deixar o espaço Eden.
Com matrizes de tipos primitivos, elas sempre são garantidas como contíguas e, portanto, você obtém a localidade de referência extremamente desejável (para o processamento seqüencial de partículas, isso faz muita diferença) e todos os benefícios que o pool de objetos se destina a fornecer. Com uma matriz de objetos, é algo análogo a uma matriz de ponteiros que começam a apontar para os objetos de maneira contígua, assumindo que você os alocou todos de uma vez no espaço Eden, mas após um ciclo de GC, podem estar apontando por todo o lado. coloque na memória.