Vale la pena usare pool di particelle in lingue gestite?


10

Stavo per implementare un pool di oggetti per il mio sistema di particelle in Java, poi ho trovato questo su Wikipedia. Per riformulare, afferma che i pool di oggetti non valgono la pena utilizzarli in linguaggi gestiti come Java e C #, poiché le allocazioni richiedono solo decine di operazioni rispetto a centinaia in linguaggi non gestiti come C ++.

Ma come tutti sappiamo, ogni istruzione può danneggiare le prestazioni del gioco. Ad esempio, un pool di client in un MMO: i client non entreranno e usciranno dal pool troppo velocemente. Ma le particelle possono essere rinnovate decine di volte in un secondo.

La domanda è: vale la pena usare un pool di oggetti per le particelle (in particolare quelle che muoiono e vengono ricreate rapidamente) in un linguaggio gestito?

Risposte:


14

Sì.

Il tempo di allocazione non è l'unico fattore. L'allocazione può avere effetti collaterali, come indurre un passaggio di garbage collection, che non solo può influire negativamente sulle prestazioni, ma può anche avere un impatto imprevedibile sulle prestazioni. Le specifiche dipenderanno dalla lingua e dalle scelte della piattaforma.

Il pool generalmente migliora anche la località di riferimento per gli oggetti nel pool, ad esempio mantenendoli tutti in array contigui. Ciò può migliorare le prestazioni durante l'iterazione del contenuto del pool (o almeno della sua parte live) poiché l'oggetto successivo nell'iterazione tenderà a trovarsi già nella cache dei dati.

La saggezza convenzionale di cercare di evitare qualsiasi allocazione nei loop di gioco più interni si applica ancora anche nelle lingue gestite (specialmente su 360, quando si utilizza XNA). Le ragioni sono leggermente diverse.


+1 Ma non hai toccato se vale la pena usare struct: in pratica non lo è (il pooling di tipi di valore non ottiene nulla) - invece dovresti avere un singolo (o possibile un insieme di) array per gestirli.
Jonathan Dickinson

2
Non ho toccato la questione della struttura da quando l'OP ha menzionato l'uso di Java e non ho familiarità con il modo in cui i tipi / strutture di valore operano in quella lingua.

Non ci sono strutture in Java, solo classi (sempre sull'heap).
Brendan Long

1

Per Java non è così utile mettere in comune gli oggetti * poiché il primo ciclo GC per gli oggetti ancora in giro li rimescolerà in memoria, spostandoli dallo spazio "Eden" e potenzialmente perdendo la località spaziale nel processo.

  • È sempre utile in qualsiasi lingua mettere in comune risorse complesse che sono molto costose da distruggere e creare thread simili. Vale la pena mettere insieme quelli perché il costo di crearli e distruggerli non ha quasi nulla a che fare con la memoria associata all'handle dell'oggetto alla risorsa. Tuttavia, le particelle non rientrano in questa categoria.

Java offre un'allocazione rapida del burst usando un allocatore sequenziale quando si allocano rapidamente oggetti nello spazio Eden. Quella strategia di allocazione sequenziale è super veloce, più veloce che mallocin C poiché sta solo raggruppando la memoria già allocata in modo sequenziale diretto, ma ha il rovescio della medaglia che non è possibile liberare singoli blocchi di memoria. È anche un trucco utile in C se vuoi semplicemente allocare le cose molto velocemente per, diciamo, una struttura di dati in cui non è necessario rimuovere nulla da esso, basta aggiungere tutto e quindi usarlo e buttare via tutto in un secondo momento.

A causa di questo inconveniente di non essere in grado di liberare singoli oggetti, il GC Java, dopo un primo ciclo, copierà tutta la memoria allocata dallo spazio Eden in nuove aree di memoria utilizzando un allocatore di memoria più lento e più generico che consente alla memoria di essere liberato in singoli pezzi in un thread diverso. Quindi può gettare via la memoria allocata nello spazio dell'Eden nel suo insieme senza disturbare i singoli oggetti che ora sono stati copiati e vivono altrove nella memoria. Dopo quel primo ciclo GC, i tuoi oggetti possono finire per essere frammentati in memoria.

Dato che gli oggetti possono finire per essere frammentati dopo quel primo ciclo GC, i vantaggi del pooling di oggetti, principalmente per il miglioramento dei modelli di accesso alla memoria (località di riferimento) e la riduzione delle spese generali di allocazione / deallocazione, sono in gran parte persi ... tanto che otterrai una migliore località di riferimento in genere allocando sempre nuove particelle e utilizzandole mentre sono ancora fresche nello spazio Eden e prima che diventino "vecchie" e potenzialmente disperse nella memoria. Tuttavia, ciò che può essere estremamente utile (come ottenere prestazioni in grado di competere con C in Java) è evitare l'uso di oggetti per le particelle e raggruppare semplici dati primitivi. Per un semplice esempio, anziché:

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

Fai qualcosa come:

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[];
}

Ora per riutilizzare la memoria per le particelle esistenti, puoi fare questo:

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;
}

Ora quando la nthparticella muore, per consentirne il riutilizzo, spingila nella lista libera in questo modo:

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

Quando aggiungi una nuova particella, vedi se riesci a far apparire un indice dalla lista libera:

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.
}

Non è il codice più piacevole con cui lavorare, ma con questo dovresti essere in grado di ottenere alcune simulazioni di particelle molto veloci con un'elaborazione sequenziale delle particelle molto compatibile con la cache, poiché tutti i dati delle particelle verranno sempre archiviati in modo contiguo. Questo tipo di rappresentante SoA riduce anche l'utilizzo della memoria poiché non dobbiamo preoccuparci del riempimento, dei metadati degli oggetti per la riflessione / invio dinamico e separa i campi caldi dai campi freddi (ad esempio, non siamo necessariamente interessati ai dati campi come il colore di una particella durante il passaggio della fisica, quindi sarebbe inutile caricarlo in una linea di cache solo per non usarlo e sfrattarlo).

Per semplificare la gestione del codice, potrebbe essere utile scrivere i propri contenitori ridimensionabili di base che memorizzano array di float, array di numeri interi e array di booleani. Ancora una volta non puoi usare generici e ArrayListqui (almeno dall'ultima volta che ho controllato) poiché ciò richiede oggetti gestiti da GC, non dati primitivi contigui. Vogliamo usare una matrice contigua di int, ad esempio, non matrici gestite da GC, le Integerquali non saranno necessariamente contigue dopo aver lasciato lo spazio Eden.

Con array di tipi primitivi, sono sempre garantiti contigui, e quindi ottieni la località di riferimento estremamente desiderabile (per l'elaborazione sequenziale delle particelle fa un mondo di differenza) e tutti i vantaggi che il pooling di oggetti è destinato a fornire. Con una matrice di oggetti, è invece in qualche modo analogo a una serie di puntatori che iniziano a indicare gli oggetti in modo contiguo supponendo che tu li abbia allocati tutti contemporaneamente nello spazio Eden, ma dopo un ciclo GC, può essere puntato su tutto il posto in memoria.


1
Questo è un bel commento sull'argomento, e dopo 5 anni di programmazione Java riesco a vederlo chiaramente; Java GC non è certamente stupido, né è stato creato per la programmazione di giochi (dal momento che non si preoccupa davvero della localizzazione dei dati e roba del genere), quindi è meglio giocare come piace a noi: P
Gustavo Maciel
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.