Несколько причин проседания производительности в .NET приложениях

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

Неправильный выбор структуры данных

Выбор структуры данных не по назначению может привести к серьезным проблемам с производительностью. Каждая структура данных обладает своими сильными и слабыми сторонами. Например, операция Contains вызванная на коллекции List<T> выполнит самый примитивный линейный поиск элемента, в то время как тот же Contains для HashSet<T> вернет результат практически мгновенно. Частое добавление объектов не в конец длинного List’а может похоронить приложение, но LinkedList<T> в такой ситуации будет чувствовать себя отлично, в особенности когда итоговое количество добавляемых элементов неизвестно заранее.

Частое выполнение некоторой операции (доступ, поиск, добавление, удаление) над коллекцией для которой она не оптимизирована чревато серьезными последствиями. Перед тем, как начать использовать некоторую структуру данных, можно подумать над тем, какие ее операции планируется использовать чаще всего, какие реже, а какие не будут использоваться вообще. Далее воспользоваться одной из табличек, которые демонстрируют сложности таких операций (например, http://bigocheatsheet.com/) и сделать взвешенный выбор в пользу наиболее подходящей структуры данных.

Обильное использование неизменяемых типов данных

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

Незнание деталей работы сборщика мусора

Сборщику мусора и устройству памяти в .NET посвящена отдельная книга Under the Hood of .NET Memory Management. Некоторые моменты устройства GC, такие как Card Table, вряд ли способны принести практическую пользу среднестатистическому разработчику, как вот мне. Однако, осведомленность о проблемах Large Object Heap, понимание того, как GC работает со слабыми ссылками и какие существуют режимы сборки мусора могут по-настоящему разогнать подтормаживающее приложение.

Также очевидно, что единственной целью GC являются объекты ссылочных типов. Следовательно, для уменьшения нагрузки на него, необходимо контролировать аллокации объектов в управляемую кучу, которые выполняет приложение. Замыкания, упаковка объектов — такие аллокации обычно выполняются неявно от программиста. В их простом обнаружении помогут Heap Allocations Viewer или Roslyn Clr Heap Allocation Analyzer. Если проблемы производительности вызваны явными аллокациями объектов оператором new, то можно рассмотреть использование структур вместо классов либо создание пула объектов (однако сначала необходимо убедиться в том, что проблема с производительностью действительно существует).

Отсутствие инициализации свойства Capacity коллекций

При создании таких коллекций как List<T>, Dictionary<TKey, TValue>, Stack<T>, Queue<T> и некоторых других generic’ов существует возможность установить свойство capacity в конструкторе:

List<int> items = new List<int>(100000);

Необходимость устанавливать свойство capacity (конечно, если оно известно заранее) заключается в том, что такие коллекции используют под капотом массивы. При переполнении массива создается новый большего размера, выполняется копирование объектов из старого, который в последствии становится мусором. Процесс повторяется каждый раз при очередном переполнении массива. Инициализация свойства capacity позволит избежать тяжеловесных повторяющихся процессов аллокации-копирования-сборки мусора, но только при условии что итоговое количество добавляемых элементов известно заранее. Если же такое количество неизвестно даже приблизительно и профайлер сигнализирует о проблеме, имеет смысл рассмотреть использование других структур, например, двунаправленного списока LinkedList<T>.

Заключение

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

Почитать дальше

Understanding different GC modes with Concurrency Visualizer
How to avoid avoiding automatic garbage collection
Top 5 .NET memory management fundamentals

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s