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

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

Определение медленных частей функции

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

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

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

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

Первое, на что стоит обратить внимание: алгоритм

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

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

Снижение сложности ваших занятий

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

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

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

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

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

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

Правильный способ реализовать этот кеш - использовать SoftReference<T>, эта ссылка будет указывать на объект до тех пор, пока на него не исчезнут сильные ссылки (поля и переменные) и когда память не будет исчерпана (во время сборки мусора). Если вам нужно знать, когда ссылка очищается, вы можете использовать ReferenceQueue<T>; обратите внимание, что при этом используется опрос, который может либо немедленно вернуться, либо ждать, пока объект не станет доступным, и поэтому, если используется очередь, его, вероятно, лучше всего разместить в своем собственном потоке.

Избегайте блокировки

Одна из худших вещей, которые вы можете сделать в своем коде, когда он должен иметь низкую задержку, - это блокировка. Эта блокировка происходит, когда вы ждете данных в другом потоке (synchronized, Object.wait() и Lock.lock()) или когда вы ждете результата от удаленного запроса (например, HTTP). Каждый раз, когда ваша программа просто сидит и ждет, чтобы что-то произошло, время потрачено впустую. Поскольку за один раз происходит только одно выполнение лямбда-выражения, действительно не будет никакой пользы, если ЦП вообще не будет использоваться, потому что ваши затраты будут одинаковыми независимо от того, сколько или сколько ЦП вы используете (если вы Thread.sleep() с большой продолжительностью это будет стоить вам, несмотря на то, что ничего не происходит).

Если это возможно в вашем коде, один из способов сократить время, затрачиваемое на блокировку, - это создать поток, который работает в фоновом режиме, а затем, когда вам действительно нужен результат, вы можете заблокировать его, пока он не станет доступным. У вас может быть рабочий объект, который хранит результат расчета и имеет внутри атомарный тип (например, AtomicInteger или AtomicObject<T>), прочтите это значение, чтобы узнать, доступен ли он, а затем, если он не заблокирован на мониторе, а затем дождитесь результата быть вычисленным, читая в цикле и игнорируя InterupptedException.

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

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

***

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

Если вы хотите узнать больше об IOpipe, попробуйте нашу 21-дневную бесплатную пробную версию! Вы также можете пообщаться с нами в сообществе IOpipe Slack.