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 malloc
in 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 nth
particella 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 ArrayList
qui (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 Integer
quali 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.