Для Java це не так корисно об'єднати об’єкти *, оскільки перший цикл GC для об'єктів, що ще існують навколо, переставить їх у пам'ять, перемістивши їх з простору "Едем" і, можливо, втратить просторову локальність у процесі.
- На будь-якій мові завжди корисно об'єднати складні ресурси, які дуже дорого знищити та створити як нитки. Вони можуть бути об'єднані, оскільки витрати на їх створення та знищення майже не мають нічого спільного з пам'яттю, пов'язаною з обробкою об'єкта до ресурсу. Однак частинки не відповідають цій категорії.
Java пропонує швидке розподіл швидких даних, використовуючи послідовний розподільник, коли ви швидко розподіляєте об'єкти в простір Eden. Ця стратегія послідовного розподілу дуже швидка, швидша, ніж malloc
у C, оскільки це просто об'єднання пам'яті, вже виділеної прямо-таки послідовним способом, але це полягає в тому, що ви не можете звільнити окремі шматки пам'яті. Це також корисний трюк в C, якщо ви просто хочете виділити речі дуже швидко для, скажімо, структури даних, де вам нічого не потрібно видаляти з неї, просто додайте все, а потім скористайтеся цим і відкиньте всю справу пізніше.
Через цей недолік неможливості звільнити окремі об'єкти, Java GC після першого циклу скопіює всю пам'ять, виділену з простору Eden, в нові регіони пам'яті, використовуючи повільніший, більш загальний цільовий розподільник пам'яті, який дозволяє пам'яті звільнятися окремими шматками в іншій нитці. Тоді він може скинути пам'ять, виділену в просторі Едена в цілому, не турбуючись з окремими об'єктами, які зараз скопійовані і живуть в іншому місці пам'яті. Після цього першого циклу GC ваші об'єкти можуть фрагментуватися в пам'яті.
Оскільки об'єкти можуть бути фрагментовані після першого циклу GC, переваги об'єднання об'єктів, коли це в першу чергу задля вдосконалення моделей доступу до пам'яті (місцеположення) та скорочення накладних витрат / розподілу, значною мірою втрачаються ... настільки багато що ти зможеш отримати кращу орієнтир, просто виділяючи нові частинки весь час та використовуючи їх, поки вони ще свіжі в просторі Едему та до того, як вони стануть "старими" та потенційно розсіяні в пам'яті. Однак, що може бути надзвичайно корисним (як, наприклад, отримання продуктивності, що конкурує з C на Java), - це уникати використання об'єктів для частинок та об'єднання простих старих примітивних даних. Для простого прикладу замість:
class Particle
{
public float x;
public float y;
public boolean alive;
}
Зробіть щось на кшталт:
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[];
}
Тепер для повторного використання пам’яті для існуючих частинок можна зробити це:
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;
}
Тепер, коли nth
частинка відмирає, щоб дозволити її повторному використанню, натисніть її на безкоштовний список так:
alive[n] = false;
next_free[n] = first_free;
first_free = n;
Додаючи нову частинку, подивіться, чи можна виводити індекс із вільного списку:
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.
}
Це не найприємніший код для роботи, але з цим ви повинні мати можливість отримати дуже швидке моделювання частинок, коли послідовна обробка частинок є дуже зручною для кешу, оскільки всі дані про частинки завжди зберігатимуться безперервно. Цей тип представників SoA також зменшує використання пам'яті, оскільки нам не потрібно турбуватися про набивання, метадані об'єкта для відображення / динамічної диспетчеризації, і вони розбивають гарячі поля від холодних полів (наприклад, ми не обов'язково стосуємося даних поля, схожі на колір частинки під час фізики, проходять, тому було б марно завантажувати його в рядок кешу, щоб не використовувати його та виселяти).
Щоб полегшити роботу з кодом, можливо, варто написати власні базові змінні контейнери, які зберігають масиви поплавців, масиви цілих чисел та масиви булевих. Знову ж, ви не можете використовувати generics і ArrayList
тут (принаймні, з останнього разу, коли я перевірив), оскільки для цього потрібні об'єкти, керовані GC, а не суміжні примітивні дані. Ми хочемо використовувати суміжний масив int
, наприклад, не керовані GC масивами, Integer
які не обов'язково будуть суміжними після виходу з простору Eden.
З масивами примітивних типів вони завжди гарантують , що вони будуть суміжними, і тому ви отримуєте вкрай бажане місце відліку (для послідовної обробки частинок це робить світ різницею) та всі переваги, які призначений для об'єднання об'єктів. З масивом об'єктів він натомість дещо аналогічний масиву покажчиків, які починають вказувати на об'єкти безперервно, припускаючи, що ви виділили їх відразу в простір Едему, але після циклу GC можна вказувати по всьому місце в пам’яті.