пользователей: 30398
предметов: 12406
вопросов: 234839
Конспект-online
РЕГИСТРАЦИЯ ЭКСКУРСИЯ

Оптимизация программных решений, методы, средства, особенности. Примеры оптимизации кода на языке Java

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

Два частных критерия оптимизации - время выполнения программы и объем используемой ею памяти.

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

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

Существуют два подхода к оптимизации программ: «чистка» и перепрограммирование. Оба подхода имеют как достоинства, так и недостатки.

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

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

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

Методы оптимизации программ:

- Обновление ПО. Это может показаться невероятным, однако переход на последнюю версию какой-нибудь используемой в проекте библиотеки, СУБД, виртуальной машины Erlang‘а или ядра Linux может очень существенно увеличить скорость работы вашего приложения. Простое и, как правило, быстрое решение.

- Настройка окружения. Используемая СУБД или операционная система могут быть настроены неправильно. Один мой коллега рассказывал, как однажды в его практике приложение удалось существенно ускорить, просто попробовав различные параметры JVM. Однако применять его, по понятным причинам, нужно после обновления. Или в случае, если обновление по каким-то причинам в обозримом будущем невозможно.

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

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

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

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

- Распределение нагрузки. Если нагрузка на СУБД мала, можно воспользоваться триггерами или хранимками, разгрузив тем самым само приложение и уменьшив трафик. Или, наоборот, можно перенести всю логику в приложение, разгрузив СУБД. Для построения отчетов, создания резервных копий и выполнения других тяжелых операций над СУБД имеет смысл завести специальную реплику. СУБД можно настроить так, чтобы разные таблицы хранились на разных физических дисках. Можно отдать пользователю статическую страницу с JavaScript и общаться с ним исключительно при помощи REST API. Пусть сам генерирует себе HTML. Статический контент можно держать на отдельном домене. Этим вы уменьшите трафик, так как на этот домен не будут отправляться кукисы.

- Ленивые вычисления. Грубо говоря, ленивые вычисления — это когда вместо конкретного значения возвращается анонимная функция, которая при вызове вычисляет это значение. В ряде языков программирования ленивые вычисления поддерживаются на уровне синтаксиса. Фокус в том, чтобы значение было вычислено непосредственно перед его использованием. Представьте себе ситуацию, когда мы отдаем данные в формате CSV и пользователь может задать фильтр, определяющий, какие столбцы должны быть переданы. В этом случае ленивые вычисления оказываются как нельзя кстати. Если окажется, что значение на самом деле не нужно, мы сэкономим время, которое было бы потрачено на его вычисление. Однако следует отметить, что ленивые вычисления приводят к увеличению объема используемой памяти и могут плохо работать с грязными функциями.

- Отложенные расчеты. Зачем считать что-то прямо сейчас, если это можно сделать потом? При обработке HTTP-запроса мы можем моментально вернуть пользователю OK, а непосредственную работу выполнить в фоновом процессе. Если запрос очень важен, мы можем положить его в персистентную очередь задач, обрабатываемую по cron’у. Или группой непрерывно работающих процессов. В последнем случае мы даже имеем хорошие шансы получить горизонтальное масштабирование и,

соответственно, реальное увеличение скорости, а не только видимое. Кроме того, отложенные задачи могут быть похожи. Например, им нужны одни и те же данные из БД. В этом случае при отложенной обработке N задач одной пачкой можно сходить в базу в N раз меньше.

- Более подходящие алгоритмы и структуры данных. Quicksort быстрее сортировки пузырьком, а эллиптические кривые быстрее RSA. Если нужно проверить принадлежность элемента множеству, следует использовать хэш-таблицы, а не односвязные списки. Правильные индексы и денормализация схемы базы данных могут существенно сократить время выполнения SQL-запросов. Если требуется синхронизировать некие данные, вместо полной их пересылки при каждом изменении лучше использовать схему снапшот + апдейты.

- Аппроксимация. Это почти что случай более подходящего алгоритма, только с потерей точности. Вместо длинной арифметики часто можно обойтись обычными float’ами. При сборе статистики данные можно слать по UDP вместо TCP. Пусть небольшая часть пакетов не дойдет, а часть — придет дважды. При сборе статистики намного важнее изменение цифр, а не конкретные значения. Также, например, незачем строить график по всем точкам, если можно взять их подмножество и построить кривую Безье. Вместо дорогостоящего вычисления медианы часто можно посчитать среднее.

- Переписывание на другой язык. Вполне может оказаться, что программу в существенной степени тормозит сборка мусора или, скажем, проверка типов на этапе выполнения. Переписывание небольших частей программы с Ruby на Scala или с Erlang на OCaml может привести к ускорению этой программы. Если переписываемый кусок кода достаточно прост, можно с небольшим риском переписать его на Си или C++. Этот метод нужно использовать крайне осторожно. Он приводит к появлению зоопарка языков программирования, что усложняет поддержку проекта. Метод может не сработать, например, из-за накладных расходов на преобразование данных из одного представления в другое. Также он может быть опасен. Например, ошибка в NIF может привести к падению всей виртуальной машины Erlang’а, а не одного процесса.

 

 

 

 

 

 

 

 

 

 

 


06.01.2018; 19:54
хиты: 175
рейтинг:0
для добавления комментариев необходимо авторизироваться.
  Copyright © 2013-2024. All Rights Reserved. помощь