Семинары Станислава Сидристого corporate blog
Programming
.NET
System Programming
C#
May 23

Оптимизация программ под Garbage Collector

Не так давно на Хабре появилась прекрасная статья Оптимизация сборки мусора в высоконагруженном .NET сервисе. Эта статья очень интересна тем, что авторы, вооружившись теорией сделали ранее невозможное: оптимизировали свое приложение, используя знания о работе GC. И если ранее мы не имели ни малейшего понятия, как этот самый GC работает, то теперь он нам представлен на блюдечке стараниями Конрада Кокоса в его книге Pro .NET Memory Management. Какие выводы почерпнул для себя я? Давайте составим список проблемных областей и подумаем, как их можно решить.


На недавно прошедшем семинаре CLRium #5: Garbage Collector мы проговорили про GC весь день. Однако, один доклад я решил опубликовать с текстовой расшифровкой. Это доклад про выводы относительно оптимизации приложений.



Снижайте кросспоколенческую связность


Проблема


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


При этом одна ссылка со старшего во младшее поколение заставляет накрывать область карточным столом:


  • 4 байта перекрывает 4 Кб или макс. 320 объектов – для x86 архитектуры
  • 8 байт перекрывает 8 Кб или макс. 320 объектов – для x64 архитектуры

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


Поэтому разреженные ссылки в младшее поколение сделают GC более трудоёмким


Решение


  • Располагать объекты со связями в младшее поколение – рядом;
  • Если предполагается трафик объектов нулевого поколения, воспользоваться пуллингом. Т.е. сделать пул объектов (новых не будет: не будет объектов нулевого поколения). И далее, "прогрев" пул двумя последовательными GC чтобы его содержимое гарантированно провалилось во второе поколение, вы избегаете тем самым ссылок на младшее поколение и имеете нули в карточном столе;
  • Избегать ссылок в младшее поколение;

Не допускайте сильной связности


Проблема


Как следует из алгоритмов фазы сжатия объектов в SOH:


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

Поэтому общая сильная связность объектов может привести к проседаниям при GC.


Решение


  • Располагать сильно-связные объекты рядом, в одном поколении
  • Избегать лишних связей в целом (например, вместо дублирования ссылок this->handle стоит воспользоваться уже существующей this->Service->handle)
  • Избегайте кода со скрытой связностью. Например, замыканий

Мониторьте использование сегментов


Проблема


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


Решение


  • При помощи PerfMon / Sysinternal Utilities проконтролировать точки выделения новых сегментов и их декоммитинг и освобождение
  • Если речь идет о LOH, в котором идёт плотный трафик буферов, воспользоваться ArrayPool
  • Если речь идет о SOH, убедиться что объекты одного времени жизни выделяются рядом, обеспечивая срабатывание Sweep вместо Collect
  • SOH: использовать пулы объектов

Не выделяйте память в нагруженных участках кода


Проблема


Нагруженный участок кода выделяет память:


  • Как результат, GC выбирает окно аллокации не 1Кб, а 8Кб.
  • Если окну не хватает места, это приводит к GC и расширению закоммиченой зоны
  • Плотный поток новых объектов заставит короткоживущие объекты с других потоков быстро уйти в старшее поколение с худшими условиями сборки мусора
  • Что приведет к расширению времени сборки мусора
  • Что приведет к более длительным Stop the World даже в Concurrent режиме

Решение


  • Полный запрет на использование замыканий в критичных участках кода
  • Полный запрет боксинга на критичных участках кода (можно использовать эмуляцию через пуллинг если необходимо)
  • Там где необходимо создать временный объект под хранение данных, использовать структуры. Лучше – ref struct. При количестве полей более 2-х передавать по ref

Избегайте излишних выделений памяти в LOH


Проблема


Размещение массивов в LOH приводит либо к его фрагментации либо к утяжелению процедуры GC


Решение


  • Использовать разделение массивов на подмассивы и класса, инкапсулирующего логику работы с такими массивами (т.е. вместо List<T>, где хранится мега-массив, свой MyList с array[][], разделяющий массив на несколько покороче)
    • Массивы уйдут в SOH
    • После пары сборок мусора лягут рядом с вечноживущими объектами и перестанут влиять на сборку мусора
  • Контролировать использования массивов double, длинной более 1000 элементов.

Где оправдано и возможно, использовать thread stack


Проблема


Есть ряд сверхкороткоживущих объектов либо объектов, живущих в рамках вызова метода (включая внутренние вызовы). Они создают трафик объектов


Решение


  • Использование выделения памяти на стеке, где возможно:
    • Оно не нагружает кучу
    • Не нагружает GC
    • Освобождение памяти — моментальное
  • Использовать Span T x = stackalloc T[]; вместо new T[] где возможно
  • Использовать Span/Memory где это возможно
  • Перевести алгоритмы на ref stack типы (StackList: struct, ValueStringBuilder)

Освобождайте объекты как можно раньше


Проблема


Задуманные как короткоживущие, объекты попадают в gen1, а иногда и в gen2.
Это приводит к утяжеленному GC, который работает дольше


Решение


  • Необходимо освобождать ссылку на объект как можно раньше
  • Если длительный алгоритм содержит код, который работает с какими-либо объектами, разнесенный по коду. Но который может быть сгруппирован в одном месте, необходимо его сгруппировать, разрешая тем самым собрать их раньше.
    • Например, на строке 10 достали коллекцию, а на строке 120 – отфильтровали.

Вызывать GC.Collect() не нужно


Проблема


Часто кажется что если вызвать GC.Collect(), то это исправит ситуацию


Решение


  • Гораздо корректнее выучить алгоритмы работы GC, посмотреть на приложение под ETW и другими средствами диагностики (JetBrains dotMemory, …)
  • Оптимизировать наиболее проблемные участки

Избегайте Pinning


Проблема


Pinning создает целый ряд проблем:


  • Усложняет сборку мусора
  • Создает пробелы свободной памяти (ноды free-list items, bricks table, buckets)
  • Может оставить некоторые объекты в более младшем поколении, образуя при этом ссылки с карточного стола

Решение


Если другого выхода нет, используйте fixed() {}. Этот способ фиксации не делает реальной фиксации: она происходит только тогда, когда GC сработал внутри фигурных скобок.


Избегайте финализации


Проблема


Финализация вызывается не детерменированно:


  • Невызванный Dispose() приводит к финализации со всеми исходящими ссылками из объекта
  • Зависимые объекты задерживаются дольше запланированного
  • Стареют, перемещаясь в более старые поколения
  • Если они при этом содержат ссылки на более младшие, порождают ссылки с карточного стола
  • Усложняя сборку старших поколений, фрагментируя их и приводя к Compacting вместо Sweep

Решение


Аккуратно вызывать Dispose()


Избегайте большого количества потоков


Проблема


При большом количестве потоков растет количество allocation context, т.к. они выделяются каждому потоку:


  • Как следствие – быстрее наступает GC.Collect.
  • Вследствие нехватки места в эфимерном сегменте вслед за Sweep наступит Collect

Решение


  • Контролировать количество потоков по количеству ядер

Избегайте траффика объектов разного размера


Проблема


При траффике объектов разного размера и времени жизни возникает фрагментация:


  • Повышение Fragmentation ratio
  • Срабатывание Collection с фазой изменения адресов во всех ссылающихся объектах

Решение


Если предполагается траффик объектов:


  • Проконтролировать наличие лишних полей, приблизив размеры
  • Проконтролировать отсутствие манипуляций со строками: там, где возможно, заменить на ReadOnlySpan/ReadOnlyMemory
  • Освобождать ссылку как можно раньше
  • Воспользуйтесь пуллингом
  • Кэши и пулы "прогревайте" двойным GC чтобы уплотнить объекты. Тем самым вы избегаете проблем с карточным столом.
+40
6.6k 98
Support the author
Comments 4
Top of the day