The 10 commandments of performance

Последнее время мне приходится очень много работать над оптимизацией скорости работы нашего продукта. Мы оптимизируем всё - использование памяти, скорость работы GUI и других компонентов, скорость взаимодействия компонентов, скорость работы в конкретных сценариях, boot time системы.

Одним из артефактов нашей деятельности стал The 10 commandments of performance - список рекоммендаций и советов, как писать наиболее оптимальный код. Его я сюда скопировать не могу, ибо NDA, но всё равно он слишком привязан к компании и конкретной системе. Поэтому я решил написать более общий список советов и рекоммендаций, как писать оптимальный код.

В этом списке вы не найдете низкоуровневых конструкций или примеров кода и алгоритмов. Это список высокоуровневых рекоммендаций.

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


1. Используйте отложенную загрузку. В основном это важно для автоматически запускаемых при старте Windows продуктов, так как позволяет сократить boot time (время загрузки системы).
Не загружайте всё сразу, а грузите модули или данные только когда необходимо.
Например, если у вас в продукте есть компонент, анализирующий сетевой трафик, то не стоит его запускать, если сети нет.
Если какой-то из ваших процессов требует загрузки большой базы данных в память, то спросите себя - Когда конкретно вам нужна эта база? Если вы можете отложить ее загрузку до первого использования, которое будет не во время boot time - это стоит сделать.
Пример - базы данных для анти-спам фильтра. Вероятность того, что пользователь начнет проверять почту еще во время boot time - минимальна, поэтому стоит отложить загрузку этих баз до первого запроса проверки почты.
Основное, что важно помнить про boot time: Любая операция, которая выполняется за 1 секунду во время обычной работы компьютера, может выполняться 10-30 секунд во время boot time. Потому что очень много процессов инициализируются одновременно.
И самое важное. Если ваш продукт установлен на 1 млн. компьютеров и он добавляет всего 5 секунд к boot time компьютера, то потери пользователей составляют десятки миллионов секунд в день. То есть, каждый день эти 5 секунд убивают годы жизни одного человека. Поэтому очень важно помнить об этом и всегда максимально сокращать boot time.

2. Ждите событий синхронизации, используя стандартные функции ожидания. Эта рекоммендация относится к многопоточным системам и системам с большим числом процессов.
Не пишите кода, который опрашивает что-то в цикле. Например такого:
while(workThread.isWorking()) {}
или еще хуже:
while(workThread.isWorking()) {Sleep(1);}
Вместо этого создайте Event и выставляйте его в том процессе или потоке, которого вы ждете и замените цикл на WaitForSingleObject.
Этой простой рекоммендацией удивительно часто пренебрегают, теряя в итоге все преимущества от использования потоков.

3. Никогда не используйте Sleep и аналоги в важных для производительности кусках кода. Любой Sleep стоит обдумать 10 раз, прежде чем написать.
Фактически, Sleep(x) - это сообщение операционной системе, что текущий поток совершенно не важен и хочет быть остановлен как минимум на x миллисекунд. Фактически почти всегда он будет остановлен на большее время. А иногда пауза может быть совсем катастрофической - сотни миллисекунд. И вы не в силах этим управлять. Поэтому используйте Sleep только в коде, для которого производительность не важна.
Грубо говоря, если вы уверены, что задержка в пол секунды для этого куска кода не критична, то в нем можно использовать Sleep.

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

5. Избегайте загрузки дубликатов кода в память. Это правило кажется очевидным, но не всё так просто.
Например, есть у вас в продукте 20 процессов. Каждый из процессов загружает по 2-5 ваших dll. Все dll слинкованы со static CRT. Дубликатов кода внутри нет. Или есть?
На самом деле вы будете иметь оверхед по нескольку сотен килобайт для каждой подключенной dll в каждом процессе за счет дубликатов статического CRT, который создает отдельные кучи для каждой dll.
В итоге это может вылиться в десятки мегабайт памяти, использованной под эти дубликаты.
В данном случае решение простое - использовать динамические библиотеки CRT.

6. Старайтесь использовать как можно меньше операций выделения и освобождения памяти в критическом для производительности коде. А лучше вообще не использовать. И добавить специальные проверки в тесты и в Debug режим, которые будут выдавать Exception при попытке выделить или удалить память в таком коде.
Про new\delete уже сломано немало копий в форумных баталиях, поэтому напишу лишь вкратце, что они сильно увеличивают фрагментацию памяти (а значит повышают вероятность Memory overflow), а также добавляют нестабильность в скорость работы алгоритма - вы не можете быть уверены в том, что new или delete отработают за небольшое константное время.
Фрагментация памяти может казаться мелкой проблемой, но представьте себе, что многие пользователи практически вообще не перегружают компьютер (например, я). А значит, если ваш процесс выделяет память 1000 раз в секунду, то он имеет все шансы получить Memory overflow на одно из выделений через несколько недель - как раз за счет фрагментации. И будьте уверены - ни один тестер вам этого не обнаружит во время тестирования.

7. Не используйте пользовательский компьютер для вычислений, которые вы можете сделать у себя.
Храните на диске данные, которые можно просто загрузить в память и они должны быть уже готовы к использованию, без дополнительных вычислений. Например, не храните большие данные в текстовых файлах (xml, txt и т.п.), которые надо парсить при загрузке. Всегда используйте бинарные файлы с готовыми к использованию данными. Создавайте такие файлы у себя, а не на компьютере пользователей при первом старте своего приложения, как это часто бывает.
Помните, что любая дополнительная операция над загружаемыми данными - это ненужная неэффективность, замедляющая загрузку.
Любые данные можно сохранить так, чтобы загрузить потом сразу в нужном формате без дополнительной обработке. И это не так сложно сделать, как обычно кажется на первый взгляд.

8. Используйте memory mapped файлы для работы с файлами всегда, когда возможно. Это самый эффективный механизм работы с файлами в Windows в большинстве случаев.

9. Всегда используйте компрессированные (сжатые) файлы и старайтесь поместить все свои файлы в один архив.
CPU работает на порядки быстрее диска, поэтому загрузить сжатый файл и разархивировать его в памяти - это иногда в разы быстрее, чем загружать несжатый файл. Очевидно, что это зависит от уровня компрессии и требует экспериментальной проверки. Но если ваш файл сжимается хотя бы на 20%, то уже можно его грузить сжатым - это будет быстрее.
Также важно помнить, что операции открытия и закрытия файлов - очень долгие в Windows. Если у вас есть 100 файлов, которые вы загружаете вместе, то всегда имеет смысл объединить их в один большой файл. И не забыть его сжать.
Для сжатия файлов можно использовать одну из бесплатных библиотек. Например, отлично себя зарекоммендовал ZLib.

10. Используйте многопоточность. Очевидно, что в будущем число ядер и процессоров на пользовательских машинах будет только увеличиваться. Грех их не использовать.
Сейчас есть множество библиотек, упрощающих работу по созданию многопоточных приложений - изучите их и используйте.
Например, OpenMP.

Дополнительные рекоммендации:

1. Используйте Application Verifier и другие тулзы для проверки своих приложений. Глупо не использовать программы, которые быстро и автоматически могут найти ошибки в вашем коде. В идеале эти тулзы должны проверять каждый новый билд автоматически.
2. Старайтесь, чтобы компьютер всегда отзывался на действия пользователя. Ибо пользователи очень не любят подвисания и могут легко анинсталировать программу из-за этого, и даже дать негативные отзывы.
3. Полностью отключайте вывод отладочной информации в файлы в релизных версиях.


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

Оригинал и комментарии

 Понравилась статья? Подпишись на RSS!

2 комментариев к The 10 commandments of performance

  • Я бы все же начал с рекомендации не заниматься преждевременной оптимизацией. Следующее правило - всегда оптимизируй самое узкое место.

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

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

      Согласен. Поэтому вначале статьи написано:

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

Ответить

 

 

 

Вы можете использовать эти HTML тэги

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>