можно ли сделать код еще более оптимизированным

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

Какая бывает и зачем нужна.

Мы тут разбираемся в важных понятиях большой разработки:

Чтобы закрыть тему, поговорим об оптимизации.

Что такое оптимизация

Оптимизация кода — это когда программист берёт код из уже готовой и работающей программы и пытается его улучшить для какой-то цели.

Что пытаются улучшить:

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

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

Оптимизация скорости работы

Самая частая оптимизация кода — повышение скорости работы.

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

Примеры того, как можно добиться ускорения:

Оптимизация скорости загрузки

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

Этим приёмом часто пользуется компания Apple: они делают свои программы так, чтобы пользователь видел интерфейс практически сразу после запуска, но реально работать с программой можно лишь через 1–2 секунды. Дело в том, что на первое место ставится скорость отрисовки и загрузки интерфейса, а остальные модули программы загружаются на фоне, и на это тоже нужно время. Зато выглядит всё так, как будто программа запустилась моментально.

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

Оптимизация скорости ответа

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

При этом такие программы могут загружаться по 20–30 секунд или даже несколько минут. Никто не ждёт от них моментальной загрузки, а вот моментальная реакция на запросы — нужна.

Оптимизация для стабильности и отказоустойчивости

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

Короче: программу должно быть очень сложно сломать.

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

Оптимизация для уменьшения объёма кода

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

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

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

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

Что дальше

Теперь мы полностью готовы работать над своим старым кодом: поддерживать легаси, делать рефакторинг и оптимизировать разные функции. Сделаем всё по очереди, но уже в другой раз.

Источник

Немного размышлений и советов по оптимизации кода на С++

можно ли сделать код еще более оптимизированным. Смотреть фото можно ли сделать код еще более оптимизированным. Смотреть картинку можно ли сделать код еще более оптимизированным. Картинка про можно ли сделать код еще более оптимизированным. Фото можно ли сделать код еще более оптимизированным

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

Как правило, язык C++ используют там, где требуется высокая скорость работы. Но на C++ без особых усилий можно получить код, работающий медленнее какого-нибудь Python/Ruby. Именно подобным кодом оперируют многочисленные сравнения Any-Lang vs C++.

Вообще, оптимизация бывает трех типов:

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

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

1. Особенности языка C++

1.1. Передача аргументов

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

1.2. Исключения

1.3. RTTI

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

1.4. Инкремент и декремент

1.5. Не создавайте временные объекты — 1

Временные объекты создают, к примеру, вот таким кодом:

1.6. Не создавайте временные объекты — 2

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

1.7. Резервирование памяти

Возвращаясь к предыдущему примеру (п. 1.5) — совсем правильный метод конкатенации должен быть таким:

1.8. Вообще избегайте лишней работы

Мне казалось, этот совет есть в любой книге для начинающего, да и базового понимания C++ должно хватать, чтобы понять… Однако вижу, что некоторые неопытные программисты на это натыкаются.

1.9. Оценивайте стоимость вызова функции в циклах for/while

Используя STL, можно не беспокоиться о том, что вызов функции дорог:

Потому что в данном случае он дешев. Это будет эквивалентно следующему коду:

1.10. Не используйте vector там, где можно было бы обойтись list или deque

С другой стороны, использование vector с предварительным резервированием (т. е. однократным выделением всей необходимой памяти) — самый быстрый и экономный способ. Потому что в случае list или deque небольшие куски памяти выделяются много раз. При выборе контейнера следует думать, какие именно операции над ним будут выполняться.

1.11. Ссылки или указатели?

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

1.12. Список инициализации конструктора

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

2. Компиляторы

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

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

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

Должно быть развернуто во что-то вроде

Вот часть ассемблерного кода, без switch :

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

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

2.2. Ленивость вычислений — 1

Следует помнить, что условия && (логическое И) и || (логическое ИЛИ) компилятор обрабатывает слева направо. При вычислении логического И, если первое условие ложно, второе даже не будет вычисляться. Соответственно, в логическом ИЛИ при истинности первого условия нет смысла вычислять второе. Вот простой пример:

Нам необходима строка больше трех символов, чтобы первым символом был y. При этом strlen(s) — дорогая операция, а s[0] == ‘y’ — дешевая. Соответственно, если поменять их местами, то, возможно, вычислять длину строки и не придется:

2.3. Ленивость вычислений — 2

bool operator && (аргумент1, аргумент2)

Следовательно, все аргументы должны быть вычислены до вызова.

2.4. Switch или if?

2.5. Ключевое слово inline

2.6. RVO — Return Value Optimization

Эта оптимизация позволяет компилятору C++ не создавать копию возвращаемого значения. Следует помочь компилятору использовать эту оптимизацию.

Поэтому одна точка выхода хоть и красивее, но менее эффективна:

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

2.7. Выравнивание структур

В объявлении классов и структур старайтесь располагать переменные по убыванию их размера. Особенно нужно уделить внимание группировке вместе переменных, чей размер меньше 8 байт. Компиляторы, в целях оптимизации, выравнивают адреса переменных, потому что обращение к переменной с типом long по выровненному адресу занимает всего один такт процессора, а если переменная не выровнена, то два такта на архитектуре i386. На некоторых архитектурах читать по невыровненному адресу вообще нельзя. Грубо говоря, невыровненная переменная располагается в нескольких ячейках памяти: первая часть в одной и часть в следующей. Так вот, благодаря этому выравниванию переменная размером 1 байт займет 4 или 8 байт. Вот иллюстрирующий пример:

На моей машине вывод будет следующий:

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

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

3. Многопоточность

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

3.1. Атомарные операции

Вообще, почти любое обращение к ядру — это дорогая операция. Как с памятью, так и со многими другими вызовами. Чем меньше таких обращений делает программа, тем лучше. В случае синхронизации дополнительные накладные расходы создает необходимость переключать контекст при конкуренции. Поэтому, если есть большая конкуренция и синхронизация выполняется с помощью мьютекса / критической секции, накладные расходы могут быть очень серьезными. И чем больше конкуренция, тем они значительнее. Вот пример плохого кода из довольно известных программ (на момент написания статьи) LinuxDC++/StrongDC++ и, вероятно, других подобных программ, основанных на одном и том же коде:

Этот код компилируется для сборки под ОС Linux. При этом для Windows код правильный:

Разница в том, что для Linux используются критические секции, а для Windows — атомарные операции, не требующие тяжелых мьютексов.

3.2. Cache Line

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

и потоки, обращающиеся к одной из переменных. Если потоки будут выполняться на разных ядрах, то произойдет событие, называемое Cache line ping-pong: когда двум разным ядрам необходимо видеть изменения друг друга и для этого приходится сбрасывать кеш и запрашивать данные из оперативной памяти. В подобных случаях, когда потокам требуются разделяемые данные, надо вставить между переменными кусок памяти, который поместится в Cache-Line процессора. Сложность в том, что размер этого Cache-Line у каждого процессора свой. Я ориентируюсь на значение 128 байт:

4. Операционные системы

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

4.1. Память

Старайтесь избегать частого выделения памяти. Это очень дорогая операция. И разница между «выделить 100 Мб» и «выделить 1 Мб» небольшая. Поэтому надо стараться организовать код так, чтобы заранее выделить большой объем памяти и использовать его без обращений к ядру ОС.

Если необходимо часто выделять память, учитывайте, что встроенный в стандартную библиотеку аллокатор неэффективен, особенно в случае активных операций с памятью из параллельных потоков. Рассмотрите возможность использования альтернативного аллокатора вроде nedmalloc или TCMalloc или пулов памяти вроде Boost.Pool.

4.2. Буферизация ввода-вывода

5. Процессоры

5.1. RAM уже давно не RAM

RAM расшифровывается как Random Access Memory. Однако на сегодняшний день попытка использовать оперативную память как источник с быстрым случайным доступом не приведет ни к чему хорошему. Потому что доступ к памяти занимает несколько сотен тактов процессора!

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

5.2. Signed или unsigned?

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

будет транслирован в

получится значительно короче:

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

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

5.3. Ветвления

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

6. Заключение

Итак, в этой статье мы рассмотрели некоторые способы оптимизации кода на С++. Надеюсь, какие-то из них оказались вам полезны. Если вы знаете другие способы, не упомянутые в статье, пишите их в комментариях!

И напоследок еще два совета:

Источник

Советы по оптимизации кода на 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

Источник

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

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