Vale a pena usar conjuntos de partículas em idiomas gerenciados?

10

Eu estava indo para implementar um pool de objetos para o meu sistema de partículas em Java, então eu encontrei isso na Wikipedia. Para reformular, ele diz que os pools de objetos não valem a pena usar em linguagens gerenciadas como Java e C #, porque as alocações levam apenas dezenas de operações em comparação com centenas em linguagens não gerenciadas como C ++.

Mas como todos sabemos, todas as instruções podem prejudicar o desempenho do jogo. Por exemplo, um pool de clientes em um MMO: os clientes não entram e saem do pool muito rápido. Mas as partículas podem se renovar dezenas de vezes em um segundo.

A questão é: vale a pena usar um pool de objetos para partículas (especificamente aquelas que morrem e são recriadas rapidamente) em uma linguagem gerenciada?

Gustavo Maciel
fonte

Respostas:

14

Sim, ele é.

O tempo de alocação não é o único fator. A alocação pode ter efeitos colaterais, como induzir uma passagem de coleta de lixo, que pode não apenas afetar negativamente o desempenho, mas também impactar o desempenho imprevisivelmente. As especificidades disso dependerão das suas opções de idioma e plataforma.

O pool também geralmente melhora a localidade de referência para os objetos no pool, por exemplo, mantendo todos em matrizes contíguas. Isso pode melhorar o desempenho ao iterar o conteúdo do pool (ou pelo menos a parte ativa), porque o próximo objeto na iteração provavelmente já estará no cache de dados.

A sabedoria convencional de tentar evitar alocações nos seus loops mais internos do jogo ainda se aplica mesmo em idiomas gerenciados (especialmente, por exemplo, no 360 ao usar o XNA). As razões para isso diferem um pouco.


fonte
+1 Mas você não mencionou se vale a pena usar estruturas: basicamente não é (o tipo de valor do pool nada alcança) - em vez disso, você deve ter um único (ou possível conjunto de) conjuntos para gerenciá-los.
Jonathan Dickinson
2
Não toquei na coisa struct desde que o OP mencionou o uso de Java e não estou tão familiarizado com a forma como os tipos / estruturas de valor operam nessa linguagem.
Não há estruturas em Java, apenas classes (sempre na pilha).
Brendan Long
1

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 mallocem 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 ArrayListaqui (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, Integerque 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.


fonte
1
Este é um ótimo artigo sobre o assunto e, após 5 anos de codificação em java, posso ver claramente; Java GC certamente não é burro, nem foi feito para programação de jogos (uma vez que does not importa realmente com a localidade de dados e outras coisas), então é melhor jogar como quiser: P
Gustavo Maciel