Способы оптимизации программного кода

Оптимизация программного кода

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

Сегодня эти мощности вызывают улыбку. Но традиции оптимизации кода сохранились. Как известно, сколько ни наращивай размер диска и объем ОЗУ, все равно будет мало. Потому написанные «неряшливо» приложения, медленные и ресурсоемкие, проигрывают конкурентную борьбу аналогам, даже если они красивы и удобны.

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

Основные принципы оптимизации

Оптимизация стоит на трех «китах» — естественность, производительность, затраченное время. Давайте разберемся подробнее, что они означают.

  1. Естественность. Код должен быть аккуратным, модульным и легко читабельным. Каждый модуль должен естественно встраиваться в программу. Код должен легко поддаваться редактированию, интегрированию или удалению отдельных функций или возможности без необходимости вносить серьезные изменения в другие части программы.
  2. Производительность. В результате оптимизации вы должны получить прирост производительности программного продукта. Как правило, удачно оптимизированная программа увеличивает быстродействие минимум на 20-30% в сравнение с исходным вариантом.
  3. Время. Оптимизация и последующая отладка должны занимать небольшой период времени. Оптимальными считаются сроки, не превышающие 10 – 15 % времени, затраченного на написание самого программного продукта. Иначе это будет нерентабельно.

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

Стоит ли применять Ассемблер

Многие разработчики искренне считают, что критические секции (некоторые называют их «узкими» местами программы) кода удобнее писать на ассемблере, так как он обеспечивает самый быстрый доступ к данным и устройствам.

Многочисленные сравнения результатов оптимизации кода на языке высокого уровня и применения ассемблерных вставок показывают, что в первом случае после компиляции программа будет работать чуть медленнее, чем при использовании ассемблера. Обычно эти цифры измеряются от 2% до 7%. Максимум – разница составляет 20%. Стоит ли ради получения столь малого эффекта тратить время и силы на написание ассемблерной версии? На наш взгляд лучше уделить больше внимания работе с кодом, который написан на ЯП высокого уровня, и оптимизировать работу алгоритма.

Как правильно оптимизировать

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

Начало оптимизации

Первое, что необходимо сделать, это выявить «узкие места» программы. Нет смысла трогать тот кусок программы, где и без вас все работает прекрасно. Здесь вы вряд ли что-то выиграете при оптимизации. В первую очередь, стоит обратить внимание на блоки кода, которые регулярно или часто повторяются в процессе работы – циклы и подпрограммы.

Пример: Если оптимизировать работу цикла хотя бы на 2% за одну итерацию, а число его повторов будет 1000 раз, в итоге мы получаем: 2% × 1000 = 2000%, вполне ощутимый результат при работе кода.

Участки кода, которые не оптимизируются

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

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

Еще раз об ассемблере

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

Оптимизировать или нет?

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

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

Заниматься оптимизацией следует только тогда, когда на программу поступают жалобы пользователей либо на этапе тестирования находятся проблемные участки, на которых программа «подвисает» и тормозит работу устройства. В таких случаях производится отладка, а для уже выпущенных в серию продуктов выпускают новые версии или, так называемые, «заплатки» (patch).

Читайте также:  Все способы соединения квадратных мотивов

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

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

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

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

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

Если все равно что-то не работает или «тормозит», придется использовать профилировщики отладочного вывода, в том числе, с учетом записи логов всех запросов SQL, если программа работает с базами данных. В случае поиска вслепую вы потратите много времени и не факт, что сможете добиться положительных результатов.

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

Настройка окружения

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

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

Также обратите внимание на саму операционную систему и мощность «железа». Может быть они вообще не предназначены для работы программного продукта, который вы пытаетесь запустить и ускорить? А, может, устарели и потому «тормозят»?

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

Избавляемся от ненужного функционала

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

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

Мемоизация

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

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

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

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

Кеширование

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

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

Распараллеливание программ

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

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

«Ленивые» вычисления

Ленивые (Lazy evaluation) или отложенные вычисления – стратегия, которую применяют в некоторых системах счисления. Суть метода заключается в том, что все расчеты откладываются до тех пор, пока не будет затребован их результат.

Такая стратегия позволит существенно снизить общий объем производимых вычислений, так как ненужные операции попросту не будут выполняться. Чтобы метод начал работать, нужно описать зависимости функций (операндов) друг от друга, что поможет отслеживать работу. В итоге вы получите код, который будет выполняться только в том случае, когда он действительно нужен.

Читайте также:  Директ маркетинг это способ

Метод приближения

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

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

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

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

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

Источник

Записки программиста

Двенадцать эффективных методов оптимизации программ

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

Для начала, немного прописных истин. Никто не занимается оптимизацией до тех пор, пока не придет заказчик (или коллега из отдела QA — не суть важно) и не скажет, что в таком-то месте программа работает слишком медленно. То есть, в первую очередь мы пишем программу с простым и понятным кодом, как следует тестируем ее и только потом, если понадобится, оптимизируем. Нет смысла оптимизировать программу, если (1) все работает и все довольны, (2) через полгода требования к программе поменяются и код придется переписать.

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

Также никто не бросается оптимизировать программу до тех пор, пока не станет понятно, насколько быстро она должна работать. Формулировка «таблица должна отрисовываться не дольше, чем за одну секунду» является правильной, а «таблица должна отрисовываться быстро» — нет. То есть, вы должны знать, в каком случае считать работу выполненной. Нельзя достичь цели, которая постоянно меняется. (Но если бизнес не хочет этого понимать, что ж… любой каприз за ваши деньги.)

Взявшись за оптимизацию, мы находим самое-самое тормозное место и ускоряем его. Если теперь программа работает достаточно быстро и ничего не сломалось, цель достигнута. Иначе переходим к первому шагу. Искать медленные места можно, к примеру, с помощью профилировщика (см perf, bcc/eBPF), сбора метрик, отладочного вывода с временными метками или логирования медленных SQL-запросов. Можно, конечно, и наугад, если в вашем распоряжении много времени.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Источник

Оцените статью
Разные способы