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

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

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

  • Свести к минимуму накладные расходы на сбор мусора и
  • уменьшить пиковый объем памяти для нижних систем.

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

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

Ответ 1

Запустите приложение с подробной сборкой мусора:

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 Virtual Machine Tuning Tuning Tuning, вероятно, полезно.

Ответ 2

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

Ответ 3

Ну, здесь есть несколько вопросов!

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

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

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

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

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

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

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

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

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

Если ваше приложение проводит много времени в цикле GC, тогда да, настройте GC, иначе это может не стоить того.

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

3 - Немного опыта

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

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

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

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

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

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

Уровень распределения снизился до 1 ГБ/с, а также частота юного GC (деленная на 3).

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

Ответ 4

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

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

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

Если вы хотите использовать выразительные объекты использования энергии. Если вы хотите, чтобы скорость (в данном случае) начиналась с native.

Ответ 9

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

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

Ответ 10

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

Ответ 11

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

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

Class MyPool
{
   LinkedList<Objects> stack;

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

Ответ 12

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

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

Существует описание wikipedia анализа escape-кода с примером того, как это работает в Java:

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

Ответ 13

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