Para Java no es tan útil agrupar objetos * ya que el primer ciclo de GC para los objetos que aún están alrededor los reorganizará en la memoria, sacándolos del espacio "Edén" y posiblemente perdiendo la localidad espacial en el proceso.
- Siempre es útil en cualquier idioma agrupar recursos complejos que son muy caros de destruir y crear como hilos. Puede valer la pena agruparlos porque el gasto de crearlos y destruirlos no tiene casi nada que ver con la memoria asociada con el identificador de objeto del recurso. Sin embargo, las partículas no se ajustan a esta categoría.
Java ofrece una asignación rápida de ráfagas utilizando un asignador secuencial cuando asigna rápidamente objetos al espacio Eden. Esa estrategia de asignación secuencial es súper rápida, más rápida que malloc
en C ya que solo agrupa la memoria ya asignada de manera secuencial directa, pero viene con la desventaja de que no puede liberar fragmentos individuales de memoria. También es un truco útil en C si solo desea asignar cosas súper rápido para, por ejemplo, una estructura de datos donde no necesita eliminar nada, simplemente agregue todo y luego utilícelo y bótelo todo más tarde.
Debido a este inconveniente de no poder liberar objetos individuales, el GC de Java, después de un primer ciclo, copiará toda la memoria asignada desde el espacio de Eden a nuevas regiones de memoria utilizando un asignador de memoria más lento y de uso general que permite que la memoria ser liberado en trozos individuales en un hilo diferente. Entonces puede tirar la memoria asignada en el espacio del Edén en su conjunto sin molestarse con objetos individuales que ahora se han copiado y viven en otro lugar en la memoria. Después de ese primer ciclo de GC, sus objetos pueden terminar fragmentados en la memoria.
Dado que los objetos pueden terminar fragmentados después de ese primer ciclo de GC, los beneficios de la agrupación de objetos cuando se trata principalmente de mejorar los patrones de acceso a la memoria (localidad de referencia) y reducir la sobrecarga de asignación / desasignación se pierden en gran medida ... tanto que obtendrá una mejor localidad de referencia, por lo general, simplemente asignando nuevas partículas todo el tiempo y usándolas mientras aún estén frescas en el espacio del Edén y antes de que se vuelvan "viejas" y potencialmente dispersas en la memoria. Sin embargo, lo que puede ser extremadamente útil (como obtener un rendimiento que rivalice con C en Java) es evitar el uso de objetos para sus partículas y agrupar datos primitivos simples y antiguos. Para un ejemplo simple, en lugar de:
class Particle
{
public float x;
public float y;
public boolean alive;
}
Haz 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[];
}
Ahora, para reutilizar la memoria de partículas existentes, puede hacer esto:
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;
}
Ahora, cuando la nth
partícula muere, para permitir que se reutilice, empújela a la lista gratuita de esta manera:
alive[n] = false;
next_free[n] = first_free;
first_free = n;
Cuando agregue una nueva partícula, vea si puede resaltar un índice de la 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.
}
No es el código más agradable para trabajar, pero con esto debería poder obtener algunas simulaciones de partículas muy rápidas con el procesamiento secuencial de partículas siempre muy amigable con el caché, ya que todos los datos de partículas siempre se almacenarán contiguamente. Este tipo de representante de SoA también reduce el uso de memoria, ya que no tenemos que preocuparnos por el relleno, los metadatos del objeto para el despacho de reflexión / dinámica, y separa los campos calientes de los campos fríos (por ejemplo, no estamos necesariamente interesados en los datos campos como el color de una partícula durante el paso de la física, por lo que sería un desperdicio cargarlo en una línea de caché solo para no usarlo y desalojarlo).
Para facilitar el trabajo con el código, puede valer la pena escribir sus propios contenedores redimensionables básicos que almacenan conjuntos de flotantes, conjuntos de enteros y conjuntos de booleanos. Nuevamente, no puede usar genéricos y ArrayList
aquí (al menos desde la última vez que lo verifiqué) ya que eso requiere objetos administrados por GC, no datos primitivos contiguos. Queremos usar una matriz contigua de int
, por ejemplo, matrices no administradas por GC, Integer
que no necesariamente serán contiguas después de dejar el espacio de Eden.
Con matrices de tipos primitivos, siempre se garantiza que son contiguas, por lo que obtienes la localidad de referencia extremadamente deseable (para el procesamiento secuencial de partículas hace una gran diferencia) y todos los beneficios que la agrupación de objetos está destinada a proporcionar. Con una matriz de objetos, en cambio es algo análogo a una matriz de punteros que comienzan apuntando a los objetos de manera contigua, suponiendo que los asignó todos a la vez en el espacio del Edén, pero después de un ciclo GC, puede apuntar por todo el colocar en la memoria.