Почему в программах следует избегать лишних обращений в память
Динамическое выделение памяти для систем с ее дефицитом — это зло. Во первых, при выделение памяти в куче ее местоположение случайно, а значит если выделить память два раза для двух объектов, то они вряд ли будут лежать в соседней памяти. Поэтому работа с такими объектами будет медленнее.
Во вторых, при постоянном выделение-освобождение памяти идет ее фрагментация. Если в системе ограниченный объем памяти, то наступит момент, когда не удастся выделить новый кусок связной памяти. Причем у вас может быть свободно 50 мегабайт, а выделить не удастся даже 10.
В третьих есть риски утечек памяти. Конечно, для их поиска и устранения вы напишите крутой менеджер памяти, но лучше выделить память один раз и дальше в ней работать, чем постоянно ее выделять динамически.
2. Используйте хранилища данных
Храните объекты вместе в одном куске памяти. Особенно если у вас есть коллекция объектов, с которыми вы будете работать одновременно. Пробегаться по такой коллекции будет значительно эффективнее, если все объекты будут последовательно в памяти.
Получение данных из памяти идет блоком в 64 байта. Это значит что нельзя получить меньше. Когда вы идете по коллекции объектов, то при получении первого объекта, получаются 64 байта начиная с первого байта объекта (это не всегда так, но в данном случае это не важно).
Если все объекты будут последовательно лежать в памяти, то при обращении ко второму объекту данные уже будут в кэше процессора и будет начата работа с ними. Если же объекты будут разбросаны в памяти, то при обращение к каждому из них придется заново обращаться к оперативной памяти.
Обращение к L1 — 20 тактов.
Обращение к L2 — 100 тактов.
Обращение к общей памяти — 600 тактов.
Поэтому нужно стремиться к написанию кода и такой организации данных, чтобы минимизировать кэшмисы, ибо они дорого обходятся если будут происходить каждый раз при работе к большими коллекциями.
Понятно, что вы не сможете выделять память под каждый объект в отдельности. Стоит иметь менеджер памяти, у которого можно запросить требуемый кусок памяти. В статье Placement new, или как создать объект в выделенной памяти я описывал как создать такой менеджер.
3. Храните одинаковые данные вместе
Допустим у вас есть некий класс. Он сложный, с большим количеством переменных. Есть так же иерархия объектов данного класса.
Что не так с этим кодом? Посмотрите на функцию. Там находится проверка на валидность данных, которая должна оптимизировать. Но эта проверка занимает 23-24 (указаны для процессора Cell, PS3) цикла процессора, в то время как вычисление выполнится за 12 тактов. Т.е. эта «оптимизация» совершенна не нужна.
Ну и конечно где нибудь будет коллекция таких объектов, например такая:
При проходе по такой коллекции для получения каждого нового элемента будет обращение к памяти. Более того, если нужно будет всего лишь найти объекты у которых m_life == false, то нужно будет грузить в память все целиком.
Решением является хранение однотипных данных в общих хранилищах. Создаем массив для Matrix4x4 и для BoundingSphere, где будут находиться данные от всех объектов. Каждый объект будет содержать указатель на свои данные. Наш объект изменится таким образом:
Одна только эта реорганизация ускорила выполнение GetBoundingSphere для всех объектов на 30 процентов. Это произошло не только потому, что размер объекта стал меньше (больше влазит в кеш) и математические данные лежат в смежной памяти, поэтому и проводить вычисления с ними значительно быстрее.
4. Работайте не с объектами а с коллекциями объектов
При работе с некоторыми коллекциями объектов нет нужды работать с объектами в отдельности. В этом случае лучше реорганизовать внутреннее устройство коллекции таким образом, чтобы вообще устранить сам объект. Допустим есть объект Ball и его коллекция Balls.
Это лучше реорганизовать так, чтобы вообще избавиться от класса Ball. Это наиболее эффективно с точки зрения использования памяти, когда однотипные данные находятся последовательно в памяти. Это значительно ускорит работу с такой коллекцией. Если иногда внешнему миру нужен экземпляр такой коллекции, то можно предусмотреть функцию GetBall.
5. Поблочное выделение памяти
Выделяйте память большими блоками, а не под каждый элемент в отдельности. Допустим вам нужно выделить память под массив таких структур и заполнить его данными (допустим из файла). Предполагается, что данные после загрузки не будут меняться:
Для начала реорганизуем структуру в соответствии с пунктом 3.
Неправильный вариант выделения памяти:
* Как видно, мы выделили память только один раз, сразу большим куском. Это уменьшает ее фрагментацию и улучшает эффективность по работе с данными.
* Проходить по массиву таких структур будет достаточно быстро (например чтобы найти элемент с нужным id), т.к. размер структуры маленький будет меньше кэшмисов.
* Проводить вычисления над данными будет так же эффективно, т.к. все данные в одном месте.
6. Учитывайте выравнивание
Учитывайте выравнивание типов. Располагайте наиболее большие типы вначале структуры. Группируйте типы с равным размером, располагая переменные подряд. Помните, что double выравнивается минимум по 8.
7. Знайте размер типов
Заведите себе подобные типы и всегда учитывайте, сколько занимают ваши переменные в памяти. Это поможет легче учитывать пункт 6.
Данные методы имеют отношение к технике, называемой Data oriented design. Можете почитать про это более подробно тут: Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf.
Программирование на языках, которые позволяют взаимодействовать с памятью на более низком уровне, как например в C и C++, иногда доставляет немало проблем, с которыми вы раньше не сталкивались, например: segfaults (ошибки сегментации). Такие ошибки очень раздражают, и могут стать причиной множества проблем; часто они свидетельствуют о том, что вы используете память, которую не следует использовать.
Одна из самых распространённых проблем — это попытка получить доступ к памяти, которая уже освобождена. Эту память вы могли освободить сами ― функцией free, или её автоматически освободила программа (например, из стека).
Поняв некоторые аспекты, связанные с памятью, вы станете действительно лучше и умнее в программировании!
Как разделена память
Память разделена на несколько сегментов, два из наиболее важных (для этой статьи) — это стек и heap. Стек — это упорядоченная область внедрения, а heap полностью произвольная ― вы резервируете память там, где это возможно.
Стек память обладает набором средств и операций для собственной работы (это место, где сохраняется информация регистров процессора). Здесь хранится информация, касающаяся работы программы, например о вызванных функциях и созданных переменных и т.д. Этой памятью управляет программа, а не разработчик.
Сегмент heap часто используется для резервирования большого объёма памяти. Этот резерв существует столько, сколько потребуется разработчику. Другими словами, контроль памяти в сегменте heap предоставлен разработчику. Разрабатывая сложные программы, часто приходится резервировать большие части памяти. Именно для этого и предназначен сегмент heap. Мы называем это ― Динамическая память.
Каждый раз, когда вы используете malloc, чтобы разместить что-либо в памяти – вы обращаетесь к сегменту heap. Любой другой вызов, например int i;, относится к стек памяти. Очень важно это понимать, чтобы с лёгкостью находить ошибки, в том числе ошибки сегментации.
Понимание стека
Ваша программа постоянно резервирует стек память, вы об этом даже не задумываетесь. Каждая функция и локальная переменная, которую вы вызываете, попадает в стек. Большинство из того, что можно сделать со стеком ― вам следует избегать, например переполнение буфера или некорректный доступ к памяти и т.д.
Как это работает изнутри?
Стек имеет структуру данных LIFO (Last-In-First-Out) ― «последним пришёл –первым ушёл». Представьте аккуратную стопку книг. В этой стопке вы можете взять первой, ту книгу, которую положили последней. Такая структура позволяет программе управлять своими операциями и пространствами, двумя простыми командами: push и pop . Команда push добавляет значение сверху стека, а pop наоборот ― изымает значение.
Для отслеживания текущего места в памяти существует специальный регистр процессора ― Stack Pointer (указатель стека). Например, переменные или обратный адрес из функции при сохранении попадают в стек и stack pointer перемещается вверх. Завершение функции означает, что всё изымается из стека, начиная с текущего положения stack pointer, до сохранённого обратного адреса из функции. Всё просто!
Чтобы проверить, всё ли вы поняли, давайте используем следующий пример (попробуйте найти ошибку самостоятельно ☺️):
Если запустить программу, она выдаст ошибку сегментации. Почему так происходит? Кажется, что всё на своих местах! За исключением… стека.
Когда мы вызываем функцию createArray, стек сохраняет обратный адрес, создаёт arr в памяти стека и возвращает arr (массив — это просто указатель на область памяти с этой информацией). Но, так как мы не использовали malloc, arr остаётся в стек памяти. После того как мы возвращаем указатель, так как мы не контролируем операции стека, программа изымает информацию из стека и использует её для своих нужд. Когда мы пытаемся заполнить массив, после того как вернули его из функции, мы повреждаем память и получаем ошибку сегментации.
Понимание кучи (heap)
В отличии от стека, в куче сохраняется что-либо, независимо от функций и пространств, в течение необходимого времени. Для использования этой памяти, в языке C есть библиотека stdlib с функциями malloc и free.
Malloc (memory allocation) запрашивает у системы необходимый объем памяти и возвращает указатель на начальный адрес. Free сообщает системе, что запрошенная память больше не нужна и может быть использована для других задач. Выглядит действительно просто ― до тех пор, пока вы избегаете ошибок.
Поскольку система не может перезаписать то, что запросил разработчик, мы должны сами управлять этим процессом с помощью этих двух функций. Это создаёт опасность возникновения утечек памяти.
Утечка произойдёт, если память не освободить вовремя, до завершения выполнения программы или если указатели на расположение этой памяти были потеряны. Программа будет использовать больше памяти чем должна. Чтобы этого не случалось, мы должны освобождать выделенную в куче память, когда она нам не нужна.
На изображении видно, как в «bad» версии кода, мы не освобождаем память. Это приводит к растрачиванию 20 * 4 байт (размер int 64-бит) = 80 байт. Может показаться, что это незначительно, но, если это большая программа, речь может идти о гигабайтах!
Чтобы программа эффективно использовала память — важно контролировать память кучи. В тоже время, делать это нужно с осторожностью. Если память была освобождена, то попытка получения доступа к ней или её использование может привести к ошибке сегментации.
Бонус: Struct и куча
Одна из распространённых ошибок при использовании struct заключается в том, что мы просто освобождаем struct. Всё прекрасно до тех пор, пока мы не выделяем память для указателей, внутри struct. В этом случае нам нужно сначала очистить их, а потом struct.
Как я устраняю утечки памяти
Программируя на C, я часто использую struct, поэтому у меня всегда под рукой две необходимые функции: конструктор и деструктор. Это единственные функции, для которых я использую mallocs и frees в struct. Это упрощает решение проблем с утечкой памяти.
Сбои в Windows - будь то синий экран смерти или полностью заблокированная система - крайне разочаровывают. Вы не только потеряете несохраненный файл или проект, который вы открыли, но и устранение причины сбоя Windows может быть затруднено.
Когда происходят сбои, вы, вероятно, задаетесь вопросом, как предотвратить эти проблемы в будущем. Давайте рассмотрим наиболее распространенные причины сбоя Windows и что делать, если сбой Windows продолжается.
1. Проблемы с ОЗУ
Поскольку ваш компьютер хранит важные процессные данные в оперативной памяти, проблемы с вашей памятью могут привести к сбою Windows. Имена ошибок, такие как Fatal Exception Error, обычно появляются, когда Windows пытается извлечь данные из памяти, но не может сделать это должным образом.
Вы можете использовать бесплатный инструмент, такой как MemTest86 , чтобы увидеть, есть ли проблемы с вашей оперативной памятью. Также стоит убедиться, что ваши ОЗУ правильно установлены в свои слоты. Обратите внимание, что, хотя нехватка ОЗУ может привести к остановке системы, обычно она не приводит к сбою Windows. Если вы уверены, что оперативная память не является причиной, иногда проблема с материнской платой может привести к аналогичным проблемам.
2. Проблемы с драйверами
Драйверы - это специализированные части программного обеспечения, которые позволяют Windows взаимодействовать с различным оборудованием, подключенным к вашему компьютеру. В большинстве случаев драйверы устанавливаются и обновляются автоматически при подключении нового периферийного устройства или запуске Центра обновления Windows.
Однако, когда драйверы работают неправильно, они могут вызвать серьезные проблемы. Вручную установка неправильного драйвера или обновление до версии с ошибкой, предоставленной производителем, являются распространенными способами для этого.
При устранении неполадок, связанных с ошибкой синего экрана, следите за упоминанием какого-либо конкретного оборудования, так как оно может быть виновником. Также откройте Диспетчер устройств (доступный по щелчку правой кнопкой мыши по кнопке «Пуск») и проверьте наличие любых предупреждающих символов, которые представляют аппаратные конфликты.
3. Отказ жесткого диска
Если накопитель (жесткий диск или твердотельный накопитель) на вашем компьютере выходит из строя, могут возникнуть сбои Windows. Это может проявиться через сбои, которые происходят только при попытке открыть определенные файлы, что указывает на то, что определенный раздел диска умирает.
Для более старых жестких дисков звук щелчка является еще одним серьезным признаком неисправного диска. Поскольку для правильной работы Windows необходим доступ к файлам на диске, она может потерпеть крах, если диск не может прочитать эти файлы. Поэтому как можно скорей сделайте резервную копию ваших файлов и по возможности поменяйте ваш жесткий диск.
4. Перегрев компьютера
Слишком много тепла вызывает серьезные проблемы для чувствительных компонентов в вашем компьютере. Система, которая слишком долго нагревается, может быть повреждена. Чтобы бороться с этим, ваш компьютер часто выключается, когда становится слишком жарко, что обычно приводит к сбою Windows.
Проблема перегрева может иметь много источников. Если у вас есть рабочий стол, убедитесь, что в вашем корпусе достаточно вентиляции. Вы также должны убедиться, что все вентиляторы внутри работают нормально и радиаторы не ослаблены. Обязательно регулярно чистите компьютер, чтобы удалить лишнюю пыль.
Если вы используете ноутбук, старайтесь не класть его на колени или на поверхности, такие как одеяла и подушки, которые могут блокировать источники охлаждения системы.
5. Вредоносные программы
Вредоносное программное обеспечение, включая вирусы, трояны и другие нежелательные файлы, может нанести ущерб вашей системе. При устранении неполадок в Windows 10 имеет смысл запускать сканирование на наличие вредоносных программ, чтобы исключить любые недоброжелательные действия.
Сканирование с помощью встроенного Защитника Windows - хороший первый вариант. Для второго варианта, я рекомендуем установить бесплатную версию Malwarebytes и запустить сканирование. Если вы обнаружите какую-либо вредоносную программу, надеюсь, что сбои исчезнут после удаления вирусов.
6. Повреждение реестра
Реестр Windows - это огромная база данных, в которой Windows и программы хранят данные. Из-за регулярного добавления, удаления и внесения изменений в записи реестра существует вероятность того, что его содержимое будет испорчено.
Некоторые неправильные конфигурации реестра незначительны, но другие могут полностью привести к сбою Windows. Вот почему мы рекомендуем избегать очистки реестра, так как они чаще всего приносят больше вреда, чем пользы. И если вы когда-либо читали руководство, которое рекомендует изменить значение реестра, будьте осторожны, чтобы вы ничего не меняли, находясь внутри.
7. Программные конфликты
Большинство ошибок программного обеспечения не приводят к сбою Windows; они влияют только на рассматриваемое приложение. Однако иногда особенно серьезные сбои программного обеспечения могут заблокировать всю систему. Если происходит сбой Windows при открытии определенного приложения, попробуйте переустановить программное обеспечение, чтобы убедиться, что оно решает проблему. .
8. Проблемы с питанием
Если вы исключили другие возможности, есть вероятность, что ваши сбои Windows происходят из-за питания вашего компьютера. Как правило, это вызвано неисправным источником питания.
Если источник питания вашего компьютера поврежден, поток энергии может колебаться или становиться слишком слабым. Это может, конечно, вызвать сбой вашего компьютера. Замена блока питания является лучшим способом устранения неполадок.
Перегрузка цепи, неисправная проводка или подключение компьютера к неисправному удлинителю может привести к сбоям из-за проблем с питанием. Чтобы проверить это, попробуйте перенести аппарат в другую комнату и посмотреть, не исчезнет ли проблема.
Считаете себя импульсивным и опрометчивым человеком, часто рубящим с плеча? Или, наоборот, нерешительным, сомневающимся?
По словам американского невролога Дэвида Перлмуттера, автора бестселлеров «Еда и мозг» и «Кишечник и мозг», есть научно доказанная причина, по которой мы принимаем неверные решения, а значит, и возможность с этим покончить!
«Способность мозга принимать правильные и, наоборот, плохие решения зависит от префронтальной коры (ПК) головного мозга. Это его наиболее развитая часть. Она находится в передней трети полушарий, прямо за лобной костью, – говорит доктор Перлмуттер. – Способность человека как вида думать, планировать будущее, рационально использовать время и общаться с окружающими в значительной степени зависит от ПК».
Когда наша префронтальная кора в хорошей форме, мы склонны принимать разумные, дальновидные решения. С другой стороны, когда ПК отключена от других частей мозга (в основном из-за факторов образа жизни, говорит доктор Перлмуттер, но об этом подробнее ниже), именно тогда наши решения становятся опрометчивыми и сфокусированными на сиюминутном удовлетворении.
Или если вы решите съесть лишний кусок пирога или купить пару дико дорогих джинсов, есть вероятность, что существует разрыв между префронтальной корой и другими областями мозга, что влияет на импульсивность принятия решения.
Вот три основные причины, по которым, согласно доктору Перлмуттеру, мы склонны ошибаться, а также три способа устранить этот недочет.
1. НЕДОСТАТОК СНА
Общеизвестно: когда вы устали, вы делаете глупые вещи. Тянетесь к пятому блинчику, хотя в холодильнике стоит овощной суп, или пропускаете тренировку, потому что нет сил подняться с дивана.
«Когда вы не выспались, вы склонны принимать решения, которые дают мгновенное удовлетворение, и это связано с префронтальной корой, – говорит невролог. – Всего после одной бессонной ночи миндалина, эмоциональный центр мозга, становится менее активной, и ее связь с ПК уменьшается. Решение одно – спать 7-8 часов».
2. СЛИШКОМ МНОГО СТРЕССА
Мы знаем, что стресс вреден для психики, но хронический стресс вреден и для физического здоровья. «Периоды длительных волнений точно вызывают сокращение нейронных связей в префронтальной коре», – объясняет доктор.
По мнению Перлмуттера, снизить уровень стресса помогут:
- Физические упражнения. Комплекс простых упражнений на растяжку смотрите в статье «Как снять стресс?» .
- Природа. Даже в городских условиях банальная прогулка в парке или сквере снижает уровень гормона стресса кортизола.
- Медитация.
Таким образом, все, что надо для принятия более грамотных решений, – это сделать несколько простых упражнений или выйти на улицу и прогуляться 20 минут. А лучше совместить то и другое: пробежка или зарядка на свежем воздухе – идеальный вариант!
3. И КУДА УЖ БЕЗ ДИЕТЫ
Работы Перлмуттера посвящены влиянию продуктов питания на деятельность мозга, в том числе и на ПК. Дэвид Перлмуттер убежден, что быстрые углеводы – хлеб и макароны, а также сахар – являются убийцами нашего мозга.
Они повреждают его сосуды, ослабляют память, повышают риск развития бессонницы, депрессий, мигреней и других неврологических нарушений вплоть до болезни Альцгеймера.
Он отмечает, что грязную работу выполняет не только хроническое воспаление, которое вызывают в организме эти продукты-убийцы. Даже острого уровня воспаления достаточно, чтобы повлиять на принятие решений!
«Вы начинаете смотреть на мир с помощью модели мгновенного удовлетворения, ориентированной на настоящее, а не на долгосрочную перспективу», – говорит Перлмуттер.
И так запускается опасный цикл: чем больше вы принимаете решений, сфокусированных на настоящем, тем больше ваше нездоровое поведение усиливает воспаление. А чем больше у вас воспалений, тем более импульсивных решений вы будете принимать. Итак, как же избежать этого воспалительного цикла?
Диета по Перлмуттеру – качественные мясо, рыба, яйца, оливковое масло, орехи, семена. Придется забыть не только о быстрых, но и о многих медленных углеводах. Злаковые запрещены вовсе, и даже к овощам следует подходить избирательно.
Отдавайте предпочтение низкоуглеводным белокочанной и цветной капусте, брокколи, сельдерею, огурцам, шпинату. В небольших количествах разрешены баклажаны, лук, чеснок, пастернак, сладкий перец, помидоры. Из фруктов допустим лишь грейпфрут и ягоды.
Оптимизация ваших привычек в еде и сне, а также то, как вы справляетесь со стрессом, непосредственно влияет на ваше здоровье. Все эти факторы связаны и зависят друг от друга, поэтому даже одно небольшое изменение принесет пользу – а значит, улучшится способность принимать правильные решения!
В современном мире разработка программного обеспечения (ПО) превратилась в одну из самых дорогостоящих индустрий, и любые ошибки и недочеты в процессе его создания могут привести к нежелательным результатам. Написание запутанного кода чревато проблематичным изменением и сопровождением готового продукта. Ошибки, не выявленные в ходе тестирования ПО, приводят к снижению надежности и затягиванию сроков его внедрения. Поэтому актуальность разработки совершенного кода очень высока, так как она позволяет повысить его надежность. Очевидно, что такой код должен быть максимально оптимальным.
Примитивный, но правильный код, написанный программистом, во многих случаях может быть усовершенствован. Чаще всего причиной является то, что выбранный алгоритм, является шаблонным и не учитывает условия поставленной задачи, то есть транслирует языковые выражения вне зависимости от их смысла в определенные последовательности команд. Формальный алгоритм не различает особые случаи и не использует их выгод. Выбор такого подхода приводит к результатам, которые лишь отчасти отвечают требованиям экономии памяти и скорости выполнения.
Для того чтобы сгенерировать код, который использует имеющиеся команды и ресурсы машины с наибольшей эффективностью, должны быть использованы более сложные схемы трансляции. Они называются оптимизациями, а использующие их компиляторы – оптимизирующими компиляторами. Так же важно придерживаться правила 10/90, которое гласит, что 10% времени потраченное на планирование до начала работы, экономит 90% времени при решении поставленных задач.
Архитектурный дизайн системы особенно сильно влияет на её производительность. Однако выбор алгоритма влияет на эффективность больше, чем любой другой элемент дизайна. Более сложные алгоритмы и структуры данных могут хорошо оперировать с большим количеством элементов, в то время как простые алгоритмы подходят для небольших объёмов данных – накладные расходы на инициализацию более сложного алгоритма могут перевесить выгоду от его использования [1, c.5].
Чем больше памяти использует программа, тем быстрее она обычно выполняется. Например, сортировка ступенчатого массива обычно выполняется построчно – программа читает каждую строку, сортирует её, а затем выводит эту строку. Такая программа хорошо экономит память, т.к. использует её только для хранения одной строки, но производительность программы обычно очень плохая. Производительность может быть значительно улучшена чтением целого файла и записью потом отсортированного результата. Однако такой способ использует больше памяти. Кэширование результата также эффективно, однако требует большего количества памяти для использования.
Цель данной работы – изучить теоретические основы оптимизации программного кода.
Поставленная цель определила следующие задачи:
Рассмотреть термин «оптимизация кода» и связанные с ним понятия.
Изучить виды и подход к оптимизации кода.
Познакомиться с методиками оптимизации кода.
ТЕРМИН «ОПТИМИЗАЦИЯ КОДА» И СВЯЗАННЫЕ С НИМ ПОНЯТИЯ
Оптимизация кода – это один из способов преобразования кода, приводящий к улучшению его характеристик и повышению производительности программы. Среди целей оптимизации можно выделить уменьшение размера кода, объема используемой оперативной памяти, повышение скорости выполнения программы, уменьшение количества операций ввода – вывода. Так как под оптимизацией понимается внесение незначительных поправок, то есть изменение одного класса, одного метода или всего лишь нескольких строк кода. Поэтому какие-либо крупные изменения проекта, приводящие к повышению производительности оптимизацией не считаются.
Существует требование, которые обычно предъявляется к методу оптимизации – оптимизированная программа должна иметь тот же результат и побочные эффекты на тех же входных данных, что и неоптимизированная программа. Тем не менее, если изменения поведения программы, не имеет большого значения на фоне выигрыша за счет использования оптимизации, то данное требование может и не играть главной роли.
Кроме того, не существует универсального решения, которое подходило бы ко всем случаям, поэтому приходится использовать альтернативные решения, для оптимизации только ключевых параметров. Как правило, необходимые ресурсы для достижения требуемого результата, то есть получения полностью оптимальной программы, которую невозможно дальше улучшить, превышают выгоду, которую можно получить, затрачивая эти ресурсы. Именно поэтому оптимальные программы не создают просто потому, что некоторый процесс оптимизации может закончиться раньше. Как показывает практика, в большинстве случаев даже при этом достигаются значительные улучшения [2, c.153].
Встречаются ситуации, когда оптимизированный код вручную, оказывается менее эффективнее кода, сгенерированного компилятором.
Каждый этап от проектирования до оптимизации кода допускает существенное повышение производительности программного обеспечения [3, c. 576].
Стоит заметить, что оптимизация кода – это не самый эффективный способ повышения производительности, более того это не самый легкий способ повысить производительность: легче купить новое оборудование или компилятор с улучшенным модулем оптимизации. Так же это не самый дешевый способ: на оптимизацию кода вручную изначально уходит много времени, а потом оптимизированный код труднее сопровождать.
Однако оптимизация кода привлекательна по ряду причин. Например, ускорить выполнение метода в 10 раз путем изменения всего лишь нескольких его строк. Кроме того, овладение мастерством написания эффективного кода – признак превращение в серьезного программиста.
Оптимизация в основном фокусируется на одиночном или повторном времени выполнения, использовании памяти, дискового пространства, пропускной способности или некотором другом ресурсе. Это обычно требует компромиссов (tradeoff) – один параметр оптимизируется за счёт других. Например, увеличение размера программного кэша чего-либо улучшает производительность времени выполнения, но также увеличивает потребление памяти. Другие распространённые компромиссы включают прозрачность кода и его выразительность, почти всегда ценой деоптимизации. Сложные специализированные алгоритмы требуют больше усилий по отладке и увеличивают вероятность ошибок.
Оптимизацию производительности следует отличать от рефакторинга. Цель рефакторинга – сделать код программы более легким для понимания. Как и оптимизация, рефакторинг обычно не изменяет поведение программы. Но оптимизация часто затрудняет понимание кода, что противоположно рефакторингу.
ВИДЫ ОПТИМИЗАЦИИ ПРОГРАММНОГО КОДА
Оптимизация кода может проводиться, как и вручную, программистом, так и автоматизировано. В последнем случае оптимизатор может быть, как отдельным программным средством, так и быть встроенным в компилятор [4, c.3].
Хороший оптимизирующий компилятор может повысить быстродействие кода на 40 и более процентов, тогда как многие из методик, используемых программистом вручную, только на 15-30%.
Существуют такие понятия как высокоуровневая и низкоуровневая оптимизация. Высокоуровневые оптимизации в большинстве проводятся программистом, который, оперируя абстрактными сущностями (функциями, процедурами, классами и т.д.) и представляя себе общую модель решения задачи, может оптимизировать дизайн системы. Оптимизации на уровне элементарных структурных блоков исходного кода (циклов, ветвлений и т.д.) тоже обычно относят к высокому уровню; некоторые выделяют их в отдельный ("средний") уровень (Н. Вирт). Низкоуровневая оптимизация производится на этапе превращения исходного кода в набор машинных команд, и зачастую именно этот этап подвергается автоматизации. Впрочем, программисты на ассемблере считают, что никакая машина не превзойдет в этом хорошего программиста (при этом все согласны, что плохой программист сделает еще хуже машины) [5].
При оптимизации кода вручную существует проблема: нужно знать не только, каким образом проводить оптимизацию, но и в каком месте её применить. Обычно из-за разных факторов (медленные операции ввода, разница в скорости работы человека-оператора и машины и т.д.) лишь 10% кода занимают целых 90% времени выполнения. Так как на оптимизацию придется расходовать дополнительное время, то вместо попыток оптимизации всей программы лучше будет оптимизировать эти "критичные" ко времени выполнения 10%. Такой фрагмент кода называют узким местом или «бутылочным горлышком» (bottleneck), и для его определения используют специальные программы - профайлеры, которые позволяют замерять время работы различных частей программы [4, c.5].
Рано начатая оптимизация кода ведет к усложнению и замедлению процесса разработки, поэтому большинство советов по улучшению кода лучше применять уже на завершающей фазе разработки, когда уже все отлажено и работает.
Главный недостаток преждевременной оптимизации - отсутствие перспективы. Это сказывается на быстродействии итогового кода, других, еще более важных атрибутах производительности и качестве программы. Если время, сэкономленное благодаря реализации наиболее простой программы, посвятит ее последующей оптимизации, итоговая программа непременно будет работать быстрее, чем программа, разработанная с использованием неорганизационного подхода к оптимизации.
Иногда оптимизация программы после ее написания не позволяет достичь нужных показателей производительности, из-за чего приходится вносить крупные изменения в завершенный код. Это значит, что оптимизация небольших фрагментов все равно не привела бы к нужным результатам. Проблема в таких ситуациях объясняется не низким качеством кода, а неподходящей архитектурой программы.
Подход выполнения оптимизации по мере написания кода, имеет массу недостатков:
• До создания полностью работоспособной программы найти узкие места в коде почти невозможно. Очень трудно догадаться, на какой участок кода приходится 50% времени выполнения, поэтому, оптимизируя код по мере написания, тратиться много времени на оптимизацию кода, который не нуждается в ней. А на оптимизацию по-настоящему важных участков времени не остается.
• В тех случаях, когда удается определить правильно узкие места, им уделяется слишком больше внимание, это может привести к появлению других узких мест. Если оптимизация выполняется после создания полной системы, разработчики могут определить все проблемные области и их относительную важность, что способствует эффективному распределению времени.
• Корректность, сокрытие информации, удобочитаемость становятся вторичными целями, хотя улучшить их потом сложнее, чем производительность.
Если оптимизацию нужно выполнить до создания полной программы, следует интегрировать процесс оптимизации в перспективу. Один из способов — это сделать, задать целевые показатели объема и быстродействия отдельных функций и провести оптимизация кода по мере его написания.
В некоторых проектах быстродействие или компактность кода действительно имеет большое значение. Однако таких проектов немного. В таких проектах проблемы с производительностью нужно решать путем предварительного проектирования. В остальных случаях ранняя оптимизация представляет серьезную угрозу для общего качества ПО, включая производительность.
Иногда методики оптимизации кода характеризуют как «практические правила» или приводят данные, говорящие о том, что определенный вид оптимизации обязательно приведет к желаемому результату. Однако, концепция «практических правил» плохо описывает саму оптимизацию кода. Единственным верным правилом является оценка результатов каждого вида оптимизации в конкретной среде. Важно убеждаться в том, что изменение, внесенное в код, не ухудшило работу программы в целом. Так как оно может привести к совершенно разным результатам в разных средах разработки.
ПОДХОД К ОПТИМИЗАЦИИ ПРОГРАММНОГО КОДА
Рассматривая целесообразность оптимизации кода, надо придерживаться следующего алгоритма [3, c.591]:
Написать хороший и понятный код, поддающийся легкому изменению
Если производительность не устраивает:
Сохранить работоспособную версию кода, чтобы позднее можно было вернуться к «последнему нормальному состоянию»
Оценить производительность системы с целью нахождения горячих точек
Выяснить, обусловлено ли плохое быстродействие неадекватным проектом, неверными типами данных или неудачным алгоритмами и определить, уместна ли оптимизация кода, если оптимизация кода неуместна, вернуться к п.1
Оптимизировать узкое место, определенное на этапе (с)
Оценить каждое улучшение.
Если оптимизация не привела к улучшению кода, вернуться к коду, сохраненному на этапе (а) (как правило, более чем в половине случаев попытки оптимизации будут приводить лишь к незначительному повышению производительности или к ее снижению)
Повторить процесс, начиная с п.2.
Исходя и вышесказанного, можно назвать несколько причин, по которым производительность не следует повышать путем оптимизации кода. Если программа должна быть универсальной, то нужно помнить, что методики, повышения производительности в одной среде, могут снижать ее в других. Если поменять компилятор то, возможно, новый компилятор будет автоматически выполнять те виды оптимизации и все усилия, выполненные вручную, окажутся бесполезными.
Таким образом, не стоит забывать проводить оптимизацию кода, по возможности применяя специализированные программные средства, но это следует делать аккуратно и с осторожностью, а иногда и приготовиться к неожиданностям от компилятора.
МЕТОДИКИ ОПТИМИЗАЦИИ КОДА
Не существует настолько общих методик, что бы можно было их применить для каждого кода. Однако ряд видов оптимизации кода можно, приспособить к конкретной задаче [6, c.79].
Виды оптимизации, похожи на виды рефакторинга, однако, рефакторинг направлен на улучшение внутренней структуры программы, а описанные ниже методы можно называть «антирефакторингом». Эти изменения ухудшают внутреннюю структуру программы ради повышения ее производительности. Если бы такие изменения не ухудшали внутреннюю структуру, они бы не считались видами оптимизации – использование их было бы по умолчанию и считалось бы методиками кодирования.
4.1 Логические выражения
Рассмотрим эффективное использование логических выражений.
• Прекращение проверки сразу же после получения ответа
Если y окажется меньше 5, то вторую проверку выполнять не нужно.
Некоторые языки поддерживают так называемую «сокращенную оценку выражений», при которой компилятор генерирует код, автоматически прекращающий проверку после получения ответа.
Если выбранный язык не поддерживает сокращенную оценку, нужно избегать операторов && и ||, используя вместо них дополнительную логику. Для сокращенной оценки код следовало бы изменить так:
Принцип прекращения проверки сразу по получении ответа уместен и других случаях. Например, исследование массива на наличие четных чисел. Можно решить эту задачу, несколькими способами. Первый способ: пройтись по всему массиву и при нахождении четного числа устанавливать флаг evenNumber. Цикл поиска может выглядеть так:
Этот способ не оптимален. Лучше было бы прекращать проверку после обнаружения первого четного числа.
Читайте также: