Найкраща практика створення мільйонів дрібних тимчасових об’єктів


109

Які "найкращі практики" для створення (і випуску) мільйонів дрібних об'єктів?

Я пишу шахову програму на Java і алгоритм пошуку генерує єдиний об’єкт «Переміщення» для кожного можливого руху, і номінальний пошук може легко генерувати понад мільйон об’єктів переміщення в секунду. JVM GC впорався з навантаженням на мою систему розробки, але мені цікаво вивчити альтернативні підходи, які б:

  1. Мінімізуйте накладні витрати на вивезення сміття та
  2. знизити піковий слід пам’яті для нижчих систем.

Переважна більшість об'єктів дуже короткочасні, але близько 1% генерованих рухів зберігаються і повертаються як збережене значення, тому будь-яка техніка об'єднання або кешування повинна забезпечити можливість виключення певних об'єктів з повторного використання. .

Я не очікую повноцінного кодового прикладу, але я буду вдячний для подальших читань / досліджень чи прикладів із відкритим кодом подібного характеру.


11
Чи підійде модель з легкою вагою для вашого випадку? en.wikipedia.org/wiki/Flyweight_pattern
Роджер Роуланд

4
Вам потрібно інкапсулювати його в об’єкт?
nhahtdh

1
Модель легкої ваги не є доцільною, оскільки об'єкти не мають спільних значущих даних. Що стосується інкапсуляції даних в об’єкт, він занадто великий, щоб бути упакованим у примітив, саме тому я шукаю альтернативи POJO.
Покірний програміст

2
Настійно рекомендую прочитати: cs.virginia.edu/kim/publicity/pldi09tutorials/…
rkj

Відповіді:


47

Запустіть додаток за допомогою багатослівного збору сміття:

java -verbose:gc

І він вам скаже, коли збирає. Було б два типи зачисток, швидкий і повний розгортки.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

Стрілка до і після розміру.

Поки це просто GC, а не повний GC, ви в безпеці додому. Звичайний GC - це колекціонер копій у "молодому поколінні", тому об'єкти, на які вже не посилаються, просто просто забуті, про що саме ви хотіли б.

Читання Java SE 6 Налаштування сміття з віртуальної машини HotSpot, ймовірно, корисне.


Експериментуйте з розміром купи Java, щоб спробувати знайти місце, де повноцінне збирання сміття зустрічається рідко. У Java 7 новий G1 GC в деяких випадках швидший (а в інших повільніше).
Майкл Шопсін

21

Починаючи з версії 6, у серверному режимі JVM використовується техніка аналізу втечі . Використовуючи його, ви можете уникнути GC разом.


1
Аналіз втечі часто розчаровує, варто перевірити, чи з'ясував JVM, що ти робиш чи ні.
Нітсан Вакарт

2
Якщо у вас є досвід використання цієї опції: -XX: + PrintEscapeAnalysis і -XX: + PrintEliminateAllocations. Це було б чудово поділитися. Бо я цього не кажу, чесно кажучи.
Михайло

див. stackoverflow.com/questions/9032519/… вам знадобиться отримати налагодження для JDK 7, я визнаю, я цього не робив, але з JDK 6 це було успішно.
Нітсан Вакарт

19

Ну, тут є кілька питань в одному!

1 - Як управляються недовговічні об’єкти?

Як було сказано раніше, JVM може прекрасно справлятися з величезною кількістю короткоживучих об'єктів, оскільки це відповідає гіпотезі слабкого покоління .

Зауважте, що ми говоримо про об'єкти, які дійшли до основної пам'яті (купи). Це не завжди так. Багато створених вами об'єктів навіть не залишають реєстру процесора. Наприклад, розглянемо це для циклу

for(int i=0, i<max, i++) {
  // stuff that implies i
}

Не будемо думати про розгортання циклу (оптимізація, яку JVM сильно виконує у вашому коді). Якщо maxвона дорівнює Integer.MAX_VALUE, виконанню циклу може знадобитися певний час. Однак iзмінна ніколи не уникне циклу блоку. Тому JVM поміщає цю змінну в регістр процесора, регулярно збільшуючи її, але ніколи не відсилає її в основну пам'ять.

Отже, створення мільйонів об’єктів не є великою справою, якщо вони використовуються лише локально. Вони будуть мертвими до зберігання в Ідені, тому GC навіть не помітить їх.

2 - Чи корисно зменшити накладні витрати GC?

Як завжди, це залежить.

По-перше, ви повинні ввімкнути журнал GC, щоб мати чітке уявлення про те, що відбувається. Ви можете ввімкнути це за допомогою -Xloggc:gc.log -XX:+PrintGCDetails.

Якщо ваша програма витрачає багато часу на цикл GC, тоді, так, налаштуйте GC, інакше це може бути не дуже вартим.

Наприклад, якщо у вас є молодий GC кожні 100 мс, який займає 10 мс, ви проводите 10% свого часу в ГК, і у вас є 10 колекцій в секунду (що становить велику оцінку). У такому випадку я б не витрачав часу на налаштування GC, оскільки ті 10 GC / s все ще будуть там.

3 - Певний досвід

У мене була схожа проблема з додатком, який створював величезну кількість даного класу. У журналах GC я помітив, що швидкість створення додатку становила близько 3 Гб / с, що занадто багато (давай ... 3 гігабайта даних щосекунди ?!).

Проблема: Занадто багато частих ГК, викликаних занадто великою кількістю об'єктів, що створюються.

У своєму випадку я приєднав профайлер пам'яті і помітив, що клас представляє величезний відсоток усіх моїх об’єктів. Я простежив моменти, щоб дізнатися, що цей клас був в основному парою булевих, загорнутих в об'єкт. У цьому випадку було доступно два рішення:

  • Переробіть алгоритм, щоб я не повертав пару булевих, але натомість у мене є два методи, які повертають кожен булевий окремо

  • Кешуйте об’єкти, знаючи, що існували лише 4 різних екземпляри

Я вибрав другий, оскільки він найменше вплинув на додаток і його було легко ввести. Потрібно було кілька хвилин, щоб поставити завод із кешем, не захищеним від потоку (безпека потоку мені не потрібна, оскільки в кінцевому підсумку у мене буде лише 4 різні екземпляри).

Швидкість розподілу знизилася до 1 ГБ / с, а також частота молодих ГК (розділена на 3).

Сподіваюся, що це допомагає!


11

Якщо у вас є лише цінні об'єкти (тобто немає посилань на інші об'єкти) і насправді, але я маю на увазі дійсно тонни і тонни їх, ви можете використовувати пряме ByteBuffersвпорядкування байтів [останнє важливо] і вам потрібно кілька сотень рядків код для виділення / повторного використання + getter / setters. Геттери схожі наlong getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

Це дозволило б вирішити проблему GC майже повністю до тих пір, поки ви виділите лише один раз, тобто величезний фрагмент, а потім керуєте об'єктами самостійно. Замість посилань у вас буде тільки індекс (тобто int) в той ByteBuffer, який повинен бути переданий разом. Можливо, вам доведеться також вирівняти пам'ять.

Ця техніка могла б використовуватись C and void*, але з деяким обгортанням це нестерпно. Мінусом продуктивності може бути перевірка меж, якщо компілятор не зможе її усунути. Основна перевага - локальність, якщо ви обробляєте кортежі як вектори, відсутність заголовка об'єкта також зменшує слід пам'яті.

Крім цього, такий підхід вам, мабуть, не знадобиться, оскільки молоде покоління практично всіх JVM помирає тривіально, а вартість розподілу - лише ударний показник. Вартість розподілу може бути трохи вище, якщо ви використовуєте finalполя, оскільки для них потрібна заборона пам'яті на деяких платформах (а саме ARM / Power), хоча на x86 це безкоштовно.


8

Якщо припустити, що ви знайдете GC - це проблема (як інші зазначають, що це не може бути), ви будете реалізовувати своє власне управління пам’яттю для вас особливим випадком, тобто класом, який зазнає великого розбиття. Дайте об’єднати об'єкти, і я вже бачив випадки, коли це працює досить добре. Реалізація об'єктів-пулів - це добре протоптана стежка, тому тут не потрібно повторно відвідувати, зверніть увагу на:

  • багатопотокова передача: використання локальних пулів ниток може працювати для вашого випадку
  • резервна структура даних: розгляньте можливість використання ArrayDeque, оскільки вона добре працює при видаленні та не має накладних витрат
  • обмежте розмір вашого басейну :)

Виміряйте до / після тощо, тощо


6

Я зустрів подібну проблему. Перш за все, спробуйте зменшити розмір дрібних предметів. Ми ввели деякі значення поля за замовчуванням, посилаючись на них у кожному об'єкті об'єкта.

Наприклад, MouseEvent має посилання на клас Point. Ми кешували точки і посилалися на них замість того, щоб створювати нові екземпляри. Те саме для, наприклад, порожніх рядків.

Іншим джерелом було кілька булевих символів, які були замінені на один int, і для кожного булевого ми використовуємо лише один байт int.


Тільки з інтересу: що це вам придбало ефективність роботи? Ви профілювали свою заявку до та після зміни, і якщо так, то які були результати?
Аксель

@Axel об'єкти використовують набагато менше пам'яті, тому GC не називається так часто. Ми, безумовно, профілювали наш додаток, але був навіть візуальний ефект від покращеної швидкості.
СтаніславЛ

6

Я мав справу з цим сценарієм з деяким кодом обробки XML деякий час тому. Я виявив, що створював мільйони об'єктів тегів XML, які були дуже маленькими (зазвичай це лише рядок) і надзвичайно короткочасними (невдача перевірки XPath означала невідповідність, тому відкидання).

Я зробив кілька серйозних тестувань і дійшов висновку, що досягти приблизно 7% покращення швидкості за допомогою списку відкинутих тегів замість створення нових. Однак після впровадження я виявив, що для отримання вільної черги потрібен механізм, щоб обрізати її, якщо вона надто велика - це повністю анулює мою оптимізацію, тому я переключив її на варіант.

Підсумовуючи це - напевно, не варто - але я радий бачити, що ти думаєш про це, це показує, що ти дбаєш.


2

З огляду на те, що ви пишете шахову програму, є кілька спеціальних прийомів, які ви можете використовувати для гідної роботи. Один простий підхід - створити великий масив довгих (або байтів) і розглянути його як стек. Кожен раз, коли ваш генератор переміщень створює рухи, він висуває пару чисел на стек, наприклад, переміщення з квадрата і переміщення в квадрат. Оцінюючи дерево пошуку, ви будете виконувати кроки та оновлювати подання на дошці.

Якщо ви хочете виразної сили, використовуйте об'єкти. Якщо ви хочете швидкості (в даному випадку), йдіть рідними.


1

Одне рішення, яке я використовував для таких алгоритмів пошуку, - це створити лише один об'єкт Move, змінити його новим рухом, а потім скасувати переміщення перед виходом із області застосування. Ви, мабуть, аналізуєте лише один крок за часом, а потім просто зберігаєте найкращий кудись.

Якщо це з інших причин неможливо, і ви хочете зменшити пікове використання пам’яті, хороша стаття про ефективність пам’яті знаходиться тут: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- посібник.pdf


Мертве посилання. Чи є інше джерело для цієї статті?
dnault

0

Просто створіть мільйони об'єктів і запишіть свій код належним чином: не зберігайте зайвих посилань на ці об’єкти. GC зробить брудну роботу за вас. Ви можете пограти з багатослівним GC, як згадувалося, щоб побачити, чи справді вони є GC'd. Java IS щодо створення та випуску об'єктів. :)


1
Вибачте, товариш, я не згоден з вашим підходом ... Ява, як і будь-яка мова програмування, полягає у вирішенні проблеми в межах її обмежень, якщо ОП обмежує GC, як ви допомагаєте йому?
Нітсан Вакарт

1
Я кажу йому, як Java насправді працює. Якщо він не в змозі уникнути ситуації, коли є мільйони тимчасових об'єктів, найкраща порада може бути, тимчасовий клас повинен бути легким, і він повинен гарантувати, що він випускає посилання якнайшвидше, ані жодного кроку більше. Я щось пропускаю?
gyorgyabraham

Java підтримує створення сміття, і очистить його для вас, це багато правдивого. Якщо ОП не може ухилитися від створення об'єктів, і він незадоволений часом, проведеним в GC, це сумне закінчення. Моє заперечення - це рекомендація, яку ви робите, щоб зробити більше роботи для GC, оскільки це якось належна Java.
Нітсан Вакарт

0

Я думаю, ви повинні прочитати про розподіл стеків у Java та аналіз втечі.

Тому що, якщо заглибитись у цю тему, ви можете виявити, що ваші об'єкти навіть не виділяються на купі, і вони не збираються GC так, як є об’єкти на купі.

Існує пояснення у вікіпедії для аналізу втечі з прикладом того, як це працює на Java:

http://en.wikipedia.org/wiki/Escape_analysis


0

Я не є великим прихильником GC, тому завжди намагаюся знайти шляхи навколо цього. У цьому випадку я б запропонував використовувати шаблон об'єктного пулу :

Ідея полягає у тому, щоб не створювати нових об’єктів, зберігаючи їх у стеку, щоб потім можна було повторно використовувати його.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}

3
Використання пулу для невеликих об'єктів є досить поганою ідеєю, для завантаження вам потрібен пул за ниткою (або загальний доступ вбиває будь-яку продуктивність). Такі басейни також працюють гірше, ніж хороший сміттєзбірник. Останнє: GC є надзвичайною справою при роботі з одночасним кодом / структурами - багато алгоритмів значно легше реалізувати, оскільки, природно, немає проблеми з ABA. Реф. підрахунок в паралельному середовищі вимагає принаймні атомної операції + забору пам'яті (LOCK ADD або CAS на x86)
bestsss

1
Управління об'єктами в басейні може бути більш дорогим , ніж дозволити запустити збирач сміття.
Thorbjørn Ravn Andersen

@ ThorbjørnRavnAndersen Взагалі я згоден з вами, але зауважте, що виявити таку різницю - досить складно, і коли ви дійдете до висновку, що GC працює у вашому випадку краще, це має бути дуже унікальний випадок, якщо така різниця має значення. Як ніколи навпаки, можливо, пул Object збереже вашу програму.
Ілля Газман

1
Я просто не розумію вашого аргументу? Дуже важко виявити, чи GC швидше об'єднання об'єктів? І тому ви повинні використовувати об'єднання об'єктів? JVM оптимізований для чистого кодування та короткотривалих об'єктів. Якщо це питання, про що йдеться у цьому питанні (яке я сподіваюся, якщо ОП генерує мільйон з них першої секунди), тоді це повинно бути лише в тому випадку, якщо є переконлива перевага для переходу на більш складну та схильну до помилок схему, як запропонована вами. Якщо це занадто важко довести, то навіщо турбуватися.
Thorbjørn Ravn Andersen

0

Об'єктні пули забезпечують величезні (десь у 10 разів) покращення щодо розподілу об'єктів на купі. Але вищевказана реалізація з використанням пов'язаного списку є і наївною, і неправильною! Зв'язаний список створює об'єкти для управління його внутрішньою структурою, що зводить нанівець зусилля. Ringbuffer, що використовує масив об'єктів, працює добре. У прикладі дайте (шахова програма, яка керує рухами) Ringbuffer має бути загорнутий у об'єкт тримача для списку всіх обчислених ходів. Тоді б передавались лише посилання на об'єкт власника рухів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.