Лучшая практика для создания миллионов небольших временных объектов

109

Каковы «лучшие практики» для создания (и выпуска) миллионов небольших объектов?

Я пишу шахматную программу на Java, и алгоритм поиска генерирует один объект «Ход» для каждого возможного хода, а номинальный поиск может легко генерировать более миллиона объектов ходов в секунду. Сборщик мусора JVM смог справиться с нагрузкой на мою систему разработки, но мне интересно изучить альтернативные подходы, которые:

  1. Минимизируйте накладные расходы на сборку мусора и
  2. уменьшить пиковую нагрузку на память для систем начального уровня.

Подавляющее большинство объектов очень недолговечны, но около 1% сгенерированных перемещений сохраняется и возвращается как постоянное значение, поэтому любой метод объединения или кэширования должен обеспечивать возможность исключения определенных объектов из повторного использования. .

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

Скромный программист
источник
11
Подходит ли шаблон наилегчайшего веса для вашего случая? en.wikipedia.org/wiki/Flyweight_pattern
Роджер Роуленд,
4
Вам нужно инкапсулировать его в объект?
nhahtdh
1
Шаблон «Легковес» не подходит, поскольку объекты не имеют общих важных данных. Что касается инкапсуляции данных в объекте, он слишком велик, чтобы быть упакованным в примитив, поэтому я ищу альтернативы POJO.
Humble Programmer
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]

Стрелка до и после размера.

Пока он просто выполняет сборку мусора, а не полную сборку мусора, вы дома в безопасности. Обычный сборщик мусора является сборщиком копий в «молодом поколении», поэтому объекты, на которые больше не ссылаются, просто забываются, а это именно то, что вам нужно.

Чтение настройки сборки мусора виртуальной машины Java SE 6 HotSpot, вероятно, будет полезным.

Нильс Бех Нильсен
источник
Поэкспериментируйте с размером кучи Java, чтобы попытаться найти точку, в которой полная сборка мусора встречается редко. В Java 7 новый сборщик мусора G1 в некоторых случаях работает быстрее (в других - медленнее).
Майкл Шопсин
21

Начиная с версии 6, в серверном режиме JVM используется метод анализа выхода . Используя его, вы можете полностью избежать сборки мусора.

Михаил
источник
1
Анализ побега часто разочаровывает, стоит проверить, поняла ли JVM, что вы делаете, или нет.
Ницан Вакарт
2
Если у вас есть опыт использования этих параметров: -XX: + PrintEscapeAnalysis и -XX: + PrintEliminateAllocations. Было бы здорово поделиться. Потому что я не знаю, честно говоря.
Михаил
см. stackoverflow.com/questions/9032519/… вам нужно будет получить отладочную сборку для JDK 7, я признаю, что не делал этого, но с JDK 6 все прошло успешно.
Nitsan Wakart
19

Что ж, тут несколько вопросов в одном!

1 - Как управляются недолговечные объекты?

Как указывалось ранее, JVM может отлично справиться с огромным количеством короткоживущих объектов, поскольку следует гипотезе слабого поколения .

Обратите внимание, что мы говорим об объектах, которые достигли основной памяти (кучи). Это не всегда так. Многие создаваемые вами объекты даже не покидают регистр ЦП. Например, рассмотрим этот цикл for

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

Давайте не будем думать о развертывании цикла (оптимизации, которую JVM серьезно выполняет в вашем коде). Если maxравно Integer.MAX_VALUE, выполнение цикла может занять некоторое время. Однако iпеременная никогда не выйдет из блока цикла. Поэтому JVM поместит эту переменную в регистр ЦП, регулярно будет увеличивать ее, но никогда не будет отправлять ее обратно в основную память.

Итак, создание миллионов объектов не представляет большого труда, если они используются только локально. Они будут мертвы до того, как будут сохранены в Эдеме, поэтому ГК их даже не заметит.

2 - Полезно ли уменьшить накладные расходы на сборщик мусора?

Как обычно, это зависит от обстоятельств.

Во-первых, вы должны включить ведение журнала GC, чтобы иметь четкое представление о том, что происходит. Вы можете включить его с помощью -Xloggc:gc.log -XX:+PrintGCDetails.

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

Например, если у вас есть молодой сборщик мусора каждые 100 мс, который занимает 10 мс, вы проводите 10% своего времени в сборщике мусора, и у вас есть 10 сборов в секунду (что очень много). В таком случае я бы не стал тратить время на настройку сборщика мусора, поскольку эти 10 сборщиков мусора в секунду все равно будут там.

3 - Некоторый опыт

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

Проблема: слишком много частых сборщиков мусора из-за создания слишком большого количества объектов.

В моем случае я подключил профилировщик памяти и заметил, что класс представляет собой огромный процент всех моих объектов. Я отследил экземпляры, чтобы выяснить, что этот класс в основном представляет собой пару логических значений, заключенных в объект. В этом случае было доступно два решения:

  • Измените алгоритм, чтобы я не возвращал пару логических значений, а вместо этого у меня было два метода, которые возвращают каждое логическое значение отдельно.

  • Кешируйте объекты, зная, что было всего 4 разных экземпляра

Я выбрал второй, так как он меньше всего повлиял на приложение и его было легко внедрить. Мне потребовалось несколько минут, чтобы создать фабрику с небезопасным кешем (потокобезопасность мне не нужна, поскольку в конечном итоге у меня будет только 4 разных экземпляра).

Скорость выделения снизилась до 1 ГБ / с, как и частота молодых GC (деленная на 3).

Надеюсь, это поможет !

Пьер Ляпорт
источник
11

Если у вас есть только объекты-значения (то есть нет ссылок на другие объекты) и на самом деле, но я имею в виду действительно тонны и тонны из них, вы можете использовать прямое ByteBuffersс собственным порядком байтов (последнее важно), и вам нужно несколько сотен строк код для распределения / повторного использования + геттеров / сеттеров. Геттеры похожи наlong getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

Это решит проблему GC почти полностью, если вы выделяете только один раз, то есть огромный кусок, а затем сами управляете объектами. Вместо ссылок у вас будет только индекс (то есть int) в то, ByteBufferчто нужно передать. Возможно, вам также потребуется выполнить выравнивание памяти самостоятельно.

Технику хотелось бы использовать C and void*, но с некоторым укутыванием это терпимо. Обратной стороной производительности может быть проверка границ, если компилятор не может ее устранить. Основным преимуществом является локальность, если вы обрабатываете кортежи как векторы, отсутствие заголовка объекта также уменьшает объем памяти.

В остальном такой подход, скорее всего, вам не понадобится, поскольку молодое поколение практически всех JVM тривиально умирает, а затраты на выделение - всего лишь скачок указателя. Стоимость распределения может быть немного выше, если вы используете finalполя, так как они требуют ограничения памяти на некоторых платформах (а именно ARM / Power), хотя на x86 это бесплатно.

bestsss
источник
8

Предполагая, что вы обнаружите, что сборщик мусора является проблемой (как указывают другие, это может быть не так), вы будете реализовывать свое собственное управление памятью для своего особого случая, то есть класса, который страдает от массового оттока. Попробуйте объединение объектов, я видел случаи, когда он работал достаточно хорошо. Реализация пулов объектов - это хорошо проторенный путь, поэтому не нужно возвращаться сюда повторно, обратите внимание на:

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

Измерьте до / после и т. Д. И т. Д.

Ницан Вакарт
источник
6

Встречал похожую проблему. Прежде всего, постарайтесь уменьшить размер мелких предметов. Мы ввели некоторые значения полей по умолчанию, ссылающиеся на них в каждом экземпляре объекта.

Например, MouseEvent имеет ссылку на класс Point. Мы кэшировали точки и ссылались на них вместо создания новых экземпляров. То же самое, например, для пустых строк.

Другим источником было несколько логических значений, которые были заменены одним int, и для каждого логического значения мы использовали только один байт int.

StanislavL
источник
Просто из интереса: что это дало вам хорошую производительность? Профилировали ли вы свое приложение до и после изменения, и если да, то каковы были результаты?
Аксель
@Axel объекты используют намного меньше памяти, поэтому GC не вызывается так часто. Однозначно мы профилировали наше приложение, но даже визуальный эффект увеличения скорости был.
StanislavL
6

Некоторое время назад я имел дело с этим сценарием с некоторым кодом обработки XML. Я обнаружил, что создаю миллионы объектов тегов XML, которые были очень маленькими (обычно просто строка) и чрезвычайно недолговечными (неудача проверки XPath означала отсутствие совпадений, поэтому откажитесь).

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

В общем - вероятно, того не стоит - но я рад видеть, что вы думаете об этом, это показывает вашу заботу.

OldCurmudgeon
источник
2

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

Если вы хотите выразительной силы, используйте предметы. Если вам нужна скорость (в данном случае), переходите на родную.

Дэвид Пламптон
источник
1

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

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

rkj
источник
Мертвая ссылка. Есть ли другой источник этой статьи?
dnault 02
0

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

gyorgyabraham
источник
1
Извините, приятель, я не согласен с вашим подходом ... Java, как и любой язык программирования, предназначен для решения проблемы в рамках своих ограничений, если OP ограничен GC, как вы ему помогаете?
Nitsan Wakart
1
Я рассказываю ему, как на самом деле работает Java. Если он не может избежать ситуации с миллионами временных объектов, лучший совет может заключаться в том, что временный класс должен быть легким, и он должен гарантировать, что он освободит ссылки как можно скорее, а не больше ни одного шага. Я что-то упускаю?
gyorgyabraham
Java поддерживает создание мусора и очистит его за вас, это правда. Если OP не может избежать создания объектов и недоволен временем, проведенным в GC, это печальный конец. Я возражаю против вашей рекомендации сделать больше работы для GC, потому что это в некотором роде правильная Java.
Nitsan Wakart
0

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

Потому что, если вы углубитесь в эту тему, вы можете обнаружить, что ваши объекты даже не выделены в куче, и они не собираются GC, как объекты в куче.

В Википедии есть объяснение анализа побега с примером того, как это работает в Java:

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

luke1985
источник
0

Я не большой поклонник GC, поэтому всегда пытаюсь найти способы обойти его. В этом случае я бы предложил использовать шаблон Object Pool :

Идея состоит в том, чтобы избежать создания новых объектов, сохранив их в стеке, чтобы вы могли повторно использовать его позже.

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ørnRavnAndersen В целом я согласен с вами, но обратите внимание, что обнаружение такой разницы является довольно сложной задачей, и когда вы все же придете к выводу, что GC работает лучше в вашем случае, это должен быть очень уникальный случай, если такая разница имеет значение. Как бы то ни было, возможно, что пул объектов сохранит ваше приложение.
Илья Газман
1
Я просто не понимаю твоих аргументов? Очень сложно определить, работает ли сборщик мусора быстрее, чем объединение объектов? И поэтому вы должны использовать пул объектов? JVM оптимизирована для чистого кодирования и недолговечных объектов. Если об этом и идет этот вопрос (что, я надеюсь, если OP сгенерирует миллион из них в секунду), то это должно быть только в том случае, если есть доказанное преимущество для перехода на более сложную и подверженную ошибкам схему, как та, которую вы предлагаете. Если это слишком сложно доказать, то зачем?
Thorbjørn Ravn Andersen
0

Пулы объектов обеспечивают огромные (иногда в 10 раз) улучшения по сравнению с распределением объектов в куче. Но приведенная выше реализация с использованием связанного списка наивна и неверна! Связанный список создает объекты для управления своей внутренней структурой, сводя на нет усилия. Кольцевой буфер, использующий массив объектов, работает хорошо. В примере give (шахматная программа, управляющая ходами) кольцевой буфер должен быть заключен в объект-держатель для списка всех вычисленных ходов. Затем будут передаваться только ссылки на объекты-держатели ходов.

Михаэль Рёштер
источник