java как оптимизировать код

Советы по оптимизации кода на Java: как не наступать на грабли

Добрый вечер, коллеги.

Перевод статьи, который мы вам предложим сегодня, призван помочь ответить на вопрос: а назрела ли необходимость целой книги по оптимизации кода на Java? Надеемся, что материал не только покажется вам интересным, но и пригодится на практике. Пожалуйста, не забудьте проголосовать.

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

Пользуйтесь профилировщиком!

Прежде, чем приступать к какой-либо оптимизации, разработчик должен убедиться, что верно оценивает производительность. Может быть, тот фрагмент кода, который кажется нам тормознутым, на самом деле просто маскирует истинный источник пробуксовки, поэтому сколько бы мы не оптимизировали «явный» источник промедления, эффект будет почти нулевым. Кроме того, нужно выбрать контрольную точку, по которой можно было бы сравнивать, дает ли ваша оптимизация какой-либо эффект, и если да – то какой.

Для достижения обеих этих целей удобнее всего пользоваться профилировщиком. В нем предусмотрены инструменты, позволяющие определить, какая именно часть вашего кода выполняется медленно, сколько времени уходит на выполнение этого кода. Могу порекомендовать два профилировщика — VisualVM (бесплатный) и JProfiler (платный – но абсолютно стоит своих денег).

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

Прежде чем попытаться перейти к точечной оптимизации конкретного пути исполнения кода, нужно подумать, по какому пути код выполняется сейчас. Иногда избранный подход бывает фундаментально ущербным – например, вы ценой неимоверных усилий и всех мыслимых оптимизаций сможете ускорить этот код на 25%, однако, если изменить подход (подобрать иной алгоритм), выполнение кода может ускориться на порядок и даже более. Зачастую такое случается, когда резко меняются масштабы данных, которые требуется обрабатывать. Бывает несложно написать решение, которое сработает в данном конкретном случае, но для работы с реальными данными оно может оказаться непригодным.

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

Сравнение потоковых API и старого доброго цикла for

Потоки – замечательное нововведение в языке Java, при позволяющее без труда переделать барахлящие фрагменты кода, отказавшись от циклов for в пользу более универсальных многоразовых блоков кода, гарантирующих уверенное выполнение. Однако, за такие удобства приходится платить: при использовании потоков снижается производительность. К счастью, эта цена, по-видимому, не слишком высока. В случае с самыми ходовыми операциями можно получить как ускорение на несколько процентов, так и замедление на 10-30%, однако, этот момент следует иметь в виду.

В 99% случаев снижение производительности при использовании потоков более чем компенсируется благодаря тому, что код становится гораздо яснее. Но в том 1% случаев, когда поток у вас, возможно, будет использоваться в очень активном цикле, стоит задуматься о некоем компромиссе в пользу производительности. Это особенно касается приложений с высокой пропускной способностью, заставляет задуматься о том, что работа с потоковыми API сопряжена с активным выделением памяти (в этой теме на StackOverflow читаем, что каждый новый фильтр отъедает еще 88 байт памяти), поэтому давление на память может возрасти. В таком случае приходится чаще запускать сборщик мусора, что очень негативно сказывается на производительности.

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

Также нужно обращать внимание, в какой именно среде выполняется ваш код. Если речь идет о сильно распараллеленном окружении (например, о сайте), то вряд ли вы ускорите его работу, добавив туда еще один поток. На самом деле, при высоких нагрузках такая ситуация может быть еще порочнее, чем непараллельное исполнение. Дело в том, что, если рабочая нагрузка по природе своей параллельна, то программа наверняка и так максимально эффективно использует оставшиеся ядра процессора – то есть, вы тратите ресурсы на разделение задач, а вычислительной мощности у вас при этом не прибавляется.

Я сделал ряд контрольных замеров. testList – это массив из 100 000 элементов, состоящий из чисел от 1 до 100 000, преобразованных в строки и затем перемешанных.

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

Передача даты и операции с ней

Мои тесты показывают, что программа выполняется до 500 раз быстрее, если просто оперировать объектом даты, нежели если парсить его, преобразовывать в строку и обратно. Даже если просто исключить этап парсинга, все равно достигается стократное ускорение. Этот пример может показаться надуманным, но, уверен, вам известны случаи, когда значения даты хранились в базе данных в виде строк, а также возвращались в виде строк в откликах API

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

Операции над строками

Манипуляция над строками – это, пожалуй, одна из самых распространенных операций в любой программе. Однако, если выполнять ее неправильно, она может получиться затратной. Именно поэтому я уделяю такое внимание работе со строками в этой статье, посвященной оптимизации Java. Ниже мы рассмотрим один из самых частых подводных камней. Однако, хочу дополнительно подчеркнуть, что такие проблемы проявляются лишь при выполнении самых скоростных фрагментов кода, либо когда приходится иметь дело с существенным количеством строк. В 99% случаев ничего из показанного ниже не случится. Однако, если такая проблема возникнет, она может убийственно сказаться на производительности.

Простейший вызов String.forma t происходит примерно в 100 раз медленнее, чем при конкатенации значений в строку вручную. Как правило, это приемлемо, поскольку на моей машине мы здесь все равно имеем дело с миллионами операций в секунду. Однако, в случае загруженного цикла, оперирующего миллионами элементов, спад производительности может быть ощутимым.

Однако, есть один случай, когда _следует _использовать именно строковое форматирование, а не конкатенацию даже в среде с высокими требованиями к производительности – я говорю о журналировании отладочной информации. Рассмотрим два вызова, происходящих в таком контексте:

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

Неиспользование построителя строк внутри цикла

Использование построителя строк вне цикла

Если у кого-нибудь есть версии, почему так происходит – поделитесь пожалуйста в комментариях.

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

Надеюсь, вам пригодятся изложенные здесь советы по оптимизации кода на Java. Еще раз подчеркну, что в большинстве контекстов описанные здесь приемы вам не пригодятся. Нет разницы, сколько раз в секунду вы успеете отформатировать строку – миллион раз или 80 миллионов раз, если вам требуется проделать всего несколько таких операций.

Но в тех критических случаях, когда речь действительно может идти о миллионах таких операций, восьмидесятикратное ускорение кода может сэкономить вам массу времени.

Написав эту статью, я собрал zip-архив со всеми упомянутыми здесь данными, и ниже привожу вывод после проверки всех контрольных точек. Все результаты получены на ПК с i5-6500. Код запускался с JDK 1.8.0_144, VM 25.144-b01 на Windows 10

Источник

Замеры производительности на Java с JMH

java как оптимизировать код. 1 tv1kJ6j72lEvdXFvaa6amQ. java как оптимизировать код фото. java как оптимизировать код-1 tv1kJ6j72lEvdXFvaa6amQ. картинка java как оптимизировать код. картинка 1 tv1kJ6j72lEvdXFvaa6amQ. Добрый вечер, коллеги.

Практически каждому разработчику известна фраза, сказанная Дональдом Кнутом в 1974 году: “Преждевременная оптимизация — корень всех зол”. Но откуда мы должны узнать, что именно стоит оптимизировать?

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

Принцип Парето

Аксиома управления бизнесом, названная в честь итальянского экономиста Вильфредо Парето, гласит:

80% продаж приходится на 20% клиентов.

То же распределение применимо ко многим областям деятельности. В информатике мы можем применить этот принцип к нашим усилиям по оптимизации. 80% фактической работы и времени приходится на 20% кода.

Среди этих 80% могут отыскаться легкодоступные плоды для нашей деятельности по оптимизации. Но мы должны сосредоточиться на более трудных 20%, если хотим добиться реального эффекта.

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

Различные виды задержки

Компьютеры — очень сложные системы. Чем дальше мы удаляемся от ядра, процессора, тем медленнее всё становится. И множество разных частей задействуется еще до того, как наш код действительно доберется до кремния.

Снижение скорости также происходит нелинейно. Как разработчики, мы должны знать, какие факторы в действительности отличают различные виды задержки, чтобы понимать, какие части стоит оптимизировать.

Разница в числах воспринимается не очень легко, поэтому представьте, что одна ссылка на кэш L1 займет 1 секунду.

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

Чтение 1 МБ из основной памяти заняло бы 50 минут, а на чтение такого же объема с SSD-накопителя ушло бы больше половины дня.
Один пакет туда и обратно между Амстердамом и Сан-Франциско путешествовал бы почти пять лет!

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

Оптимизация компилятора и среды выполнения

Одним из самых больших врагов оценки производительности являются компиляторы и среды выполнения.

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

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

В результате мы не можем проверить код, просто запустив его несколько раз в цикле, а затем измерив с помощью секундомера время после вызова метода.

Java Microbenchmark Harness

Самый простой способ по-настоящему проверить свой код — это Java Microbenchmark Harness (JMH). Он помогает оценить фактическую производительность, принимая во внимание прогрев JVM и оптимизацию кода, которые могут сделать результат неясным.

Знакомство с Harness

JMH стал де-факто стандартом для тестов производительности и был включен в JDK 12. До этой версии зависимости нужно добавлять вручную:

Мы можем запустить бенчмарк с помощью IDE или даже предпочитаемой системы сборки:

Создание теста производительности (бенчмарка)

Это так же просто, как создать модульный тест: создайте новый файл, добавьте туда метод benchmark с аннотацией @Benchmark и основную оболочку для его запуска:

Конечный результат выглядит примерно так:

Типы бенчмарков

Доступны следующие четыре типа тестов производительности:

Работа с JVM

Сколько времени?

Мы можем указать, за какую единицу времени нужно выводить результаты, добавив аннотацию @OutputTimeUnit( ) :

Управление состояниями

Если мы используем класс состояния в методе бенчмарка, JMH установит параметры соответственно и запустит тест для каждого значения:

Лучшие практики

Чтобы от замеров производительности была польза, они должны уметь обойти оптимизацию JVM, или мы просто проверим, насколько хороша JVM, а не наш код.

Мертвый код

JVM способна определить, присутствует ли у вас мертвый код, и удалить его:

Переменная result ни разу не используется в коде, поэтому это по факту — мертвый код, и все три строки внутри бенчмарка будут удалены.

Есть два варианта, чтобы заставить JVM не убирать мертвый код:

Оптимизация постоянных величин

Даже если мы будем возвращать result или использовать Blackhole, чтобы предотвратить удаление мертвого кода, JVM может оптимизировать значения постоянных. Это сводит наш код к чему-то вроде этого:

Предоставление класса состояния не дает JVM “оптимизировать” (выкидывать) постоянные величины:

Небольшие модули

Измерение производительности во многом напоминает модульное тестирование. Не стоит проводить тесты или замеры над большими элементами кода. Чем меньше элементы кода, тем меньше возможные побочные эффекты. Нам нужно свести к минимуму все, что может загрязнить результаты замеров производительности.

(Почти) Продакшен

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

Например, моя среда для разработки на Java состоит из нескольких контейнеров Docker на одной машине (Eclipse, MySQL, MongoDB, RabbitMQ), а также некоторых других контейнеров (ELK-Stack, Postgres, Killbill, MariaDB). Все они делят одни и те же 32 ГБ оперативной памяти и 8 потоков процессора. Продакшен, в свою очередь, распределяется между несколькими хостами, с меньшим количеством контейнеров и удвоением потоков оперативной памяти и процессора, а также конфигурацией SSD RAID 1.

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

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

Заключение

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

Корень оптимизации заключается в том, чтобы перестать беспокоиться о ненужных вещах. Действительно ли результаты ваших замеров производительности применимы к выполнению кода в реальности? Взгляните на общую картину и сосредоточьтесь на реальных проблемах, таких как оптимизация доступа к данным, алгоритмов и структур данных.

В этой статье мы только поверхностно коснулись темы замеров производительности с помощью JMH. Он послужит мощным дополнением к вашему набору инструментов.

Источник

Оптимизация производительности Java приложений

java как оптимизировать код. javaoprimization. java как оптимизировать код фото. java как оптимизировать код-javaoprimization. картинка java как оптимизировать код. картинка javaoprimization. Добрый вечер, коллеги.Не смотря на то, что описываемые ниже трюки и советы работают не только в J2ME, именно для мобильных приложений они имеют первостепенное значение в силу ограниченности ресурсов платформы.

Техникиоптимизации производительности, как правило, основаны на увеличении объема памяти, необходимой программе для работы. К сожалению, ресурсы платформы Java ME очень ограничены, и программисту приходится постоянно балансировать между производительностью и экономией системных ресурсов. На мой взгляд, рано начатая оптимизация кода ведет к усложнению и замедлению процесса разработки, поэтому большинство приведенных тут советов лучше применять уже на завершающей фазе разработки, когда уже все отлажено и работает.

1. Избегайте синхронизации

Известно, что код, в котором используется механизм синхронизации, примерно в 4 раза медленнее обычного кода. Независимо от конкретной реализации Java VM использование синхронизации требует от виртуальной машины большого количества дополнительных действий: она должна отслеживать блокировки, блокировать контекст при начале работы с ним и разблокировать, когда работа с контекстом закончена. Потоки, которые хотят получить доступ к заблокированному контексту вынуждены стоять в очереди и ждать его освобождения. Думаю, что привел достаточно убедительные доводы, и Вы будете использовать синхронизацию только там, где без нее действительно невозможно обойтись.

2. Используйте предварительные вычисления

Если Вы разрабатываете игру с 3D или 2.5D графикой, то наверняка используете массу математических вычислений с тригонометрическими функциями. Такие расчеты сильно нагружают процессор, поэтому стоит заранее просчитать наиболее сложные выражения и представить их в виде массива, откуда доставать готовые значения в процессе выполнения программы. Помимо графики существует масса приложений, где предварительные вычисления можно сделать заранее и оформить в виде массивов данных.

3. Вытягивание массивов

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

4. Разворачивание циклов for

Выполняется виртуальной машиной следующим образом:

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

Если вы на этапе программирования точно знаете число необходимых итераций, Вы можете частично развернуть цикл:

Эта реализация будет повторяться всего 3 раза, соответственно накладные расходы уменьшатся в 5 раз по сравнению с предыдущим примером.

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

5. Сжатие циклов for

Смысл операции сжатия заключается в вынесении за рамки цикла всего того, что не нуждается в повторном вычислении:

Во втором примере переменным a и b значение присваивается всего один раз. Таким образом, по сравнению с первым вариантом нам удалось избавиться от 20 лишних операций присваивания значений.

Приведу еще одну менее очевидную технику сжатия циклов. Никогда не вызывайте методы вычисления размеров в заголовке цикла. Сравните:

6. Избегайте интерфейсных вызовов методов

В байткоде Java существует 4 типа методов. Ниже они перечислены в порядке уменьшения скорости вызова.

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

invokeinterface
Требуют поиск подходящей реализации интерфейсного метода.

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

7. Избегайте ненужных обращений к массиву

8. Избегайте использования аргументов

При вызове нестатических методов вы неявно передаете ссылку this вместе с другими параметрами. В Java VM вызов методов реализован по принципу стека. При каждом вызове аргументы заносятся в стек, а затем извлекаются из него при выполнении метода. В некоторых случаях можно избежать необходимости использования стека, отказавшись от передачи аргументов.

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

9. Откажитесь от использования локальных переменных

Локальные переменные помещаются и извлекаются из стека при вызове каждого метода. С точки зрения производительности гораздо эффективнее использовать обычные переменные.

10. Не используйте getter-ы/setter-ы

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

11. Эффективная математика

Этими методами оптимизации пользовались еще праотцы, когда писали первые игры под DOS. Для Java они тоже подходят.

12. Пишите кратко

Применяйте операторы типа +=, поскольку они генерируют короткий байткод.

13. Используйте встроенные методы

Пользуйтесь методами, предоставляемыми платформой. Например, использование System.arraycopy для копирования элементов массива будет более эффективным, чем аналогичная собственная реализация.

14. Используйте StringBuffer вместо String

Если Вы работаете со строками, значения которых могут меняться по ходу выполнения программы, используйте класс StringBuffer вместо String. Любое изменение объекта String приводит к созданию нового объекта.

Заключение

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

Источник

Как не мусорить в Java

Существует популярное заблуждение о том, что если не нравится garbage collection, то надо писать не на Java, а на C/C++. Последние три года я занимался написанием low latency кода на Java для торговли валютой, и мне приходилось всячески избегать создания лишних объектов. В итоге я сформулировал для себя несколько простых правил, как свести аллокации в Java если не до нуля, то до некого разумного минимума, не прибегая к ручному управлению памятью. Возможно, кому-то из сообщества это тоже будет полезно.

Зачем вообще избегать создания мусора

О том, какие есть GC и как их настраивать говорилось и писалось много. Но в конечном счете как ни настраивай GC — код, который мусорит, будет работать субоптимально. Всегда возникает компромисс между throughput и latency. Становится невозможно улучшить одно не ухудшив другое. Как правило накладные расходы GC измеряют изучая логи — по ним можно понять в какие моменты были паузы и сколько времени они занимали. Однако в логах GC содержится далеко не вся информация об этих накладных расходах. Объект, созданный потоком, автоматически помещается в L1 кэш ядра процессора, на котором выполняется данный поток. Это приводит к вытеснению оттуда других потенциально полезных данных. При большом количестве аллокаций полезные данные могут быть вытеснены и из L3 кэша. Когда в следующий раз поток будет обращаться к этим данным произойдет кэш мисс, что приведет к задержкам в исполнении программы. Более того, так как L3 кэш является общим для всех ядер в пределах одного процессора, мусорящий поток будет выталкивать из L3 кэша данные и других потоков/приложений, и уже они будут сталкиваться с лишними дорогостоящими кэш миссами, даже если сами они написаны на голом С и мусор не создают. Никакие настройки никаких garbage collector’ов (ни C4, ни ZGC) не помогут справиться с этой проблемой. Единственный способ улучшить ситуацию в целом — это не создавать лишние объекты без надобности. Java в отличие от C++ не имеет богатого арсенала механизмов работы с памятью, но тем не менее есть ряд способов, позволяющих свести аллокации к минимуму. О них и пойдет речь.

Разумеется, не нужно писать весь код в стиле garbage free. Фишка языка Java как раз в том, что можно сильно упростить себе жизнь, убирая только основные источники мусора. Можно также не заниматься safe memory reclamation при написании lock-free алгоритмов. Если некий код выполняется только один раз при старте приложения, то он может аллоцировать сколько угодно, и это не страшно. Ну и разумеется, основной рабочий инструмент при избавлении от лишнего мусора — это allocation profiler.

Использование примитивных типов

Пример 1. Обычные вычисления

Допустим, у нас есть обычная функция, которая просто что-то считает.

Пример 2. Лямбды

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

Несмотря на то, что переменная x является примитивом, будет создан объект типа Integer, который будет передан в calculator. Чтобы этого избежать, надо использовать IntConsumer вместо Consumer :

Пример 3. Коллекции

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

Mutable объекты

А что делать, если примитивами обойтись не получается? Например в том случае, если нужный нам метод должен вернуть несколько значений. Ответ простой — использовать mutable объекты.

В некоторых языках делается упор на использование immutable объектов, например в Scala. Основной аргумент в их пользу заключается в том, что сильно упрощается написание многопоточного кода. Тем не менее, имеются и накладные расходы, связанные с избыточной аллокацией мусора. Если мы хотим этого их избежать, то нам не следует создавать короткоживущие immutable объекты.

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

Как можно избавиться от аллокации в этом случае? Правильно, передать IntPair в качестве аргумента и записать туда результат. В этом случае надо написать подробный javadoc, а еще лучше использовать некую конвенцию для названий переменных, куда записывается результат. Например, их можно начинать с префикса out. Garbage free код в этом случае будет выглядеть так:

Для лаконичности я не стал писать “лишний” код по обработке ошибок, корректному завершению работы программы и т.д. Основная идея этого куска кода заключается в том, что используемый нами объект IntPair создается один раз и сохраняется в final поле.

Объектные пулы

Когда мы пользуемся mutable объектами мы должны сначала откуда-то взять пустой объект, потом записать в него нужные нам данные, попользоваться ими где-то, а затем вернуть объект “на место”. В вышеописанном примере объект всегда был “на месте”, т.е. в final поле. К сожалению, это не всегда получается сделать простым образом. Например, мы можем заранее не знать, сколько именно объектов нам понадобится. В этом случае нам на помощь приходят объектные пулы. Когда нам становится нужен пустой объект, мы достаем его из объектного пула, а когда он перестает быть нужен, мы его туда возвращаем. Если в пуле нет свободного объекта, то пул создает новый объект. Это уже по факту является ручным управлением памятью со всеми вытекающими последствиями. К этому способу желательно не прибегать, если есть возможность пользоваться предыдущими способами. Что может пойти не так?

Для того чтобы уменьшить вероятность совершения описанных выше ошибок можно использовать стандартную конструкцию try-with-resources. Выглядеть это может так:

Метод divide может выглядеть так:

А метод listenSocket вот так:

В IDE как правило можно настроить подсвечивание всех случаев использования AutoCloseable объектов вне try-with-resources блока. Но это не стопроцентный вариант, т.к. подсвечивание в IDE может быть просто выключено. Поэтому есть еще один способ гарантировать возврат объекта в пул — инверсия контроля. Приведу пример:

В этом случае мы в принципе не можем получить доступ к объекту класса IntPair снаружи. К сожалению, этот способ тоже работает не всегда. Например, он не будет работать в случае если один поток достает объекты из пула и кладет в некую очередь, а другой поток достает их из очереди и возвращает в пул.

Дополнительной проблемой здесь является многопоточность. Реализация объектного пула должна быть очень быстрой, чего довольно сложно добиться. Медленный пул может принести больше вреда для производительности, чем пользы. В свою очередь аллокация новых объектов в TLAB происходит очень быстро, гораздо быстрее, чем malloc в C. Написание быстрого объектного пула — это отдельная тема, которую я бы сейчас не хотел развивать. Скажу только, что хороших «готовых» реализаций я не видел.

Вместо заключения

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

Update:

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *