Хабрахабр

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

NET сервисе. Не так давно на Хабре появилась прекрасная статья Оптимизация сборки мусора в высоконагруженном . И если ранее мы не имели ни малейшего понятия, как этот самый GC работает, то теперь он нам представлен на блюдечке стараниями Конрада Кокоса в его книге Pro . Эта статья очень интересна тем, что авторы, вооружившись теорией сделали ранее невозможное: оптимизировали свое приложение, используя знания о работе GC. Какие выводы почерпнул для себя я? 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 приводит либо к его фрагментации либо к утяжелению процедуры GC

Решение

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

Проблема

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

Решение

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

Проблема

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

Решение

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

Проблема

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

Решение

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

Проблема

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

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

Решение

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

Проблема

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

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

Решение

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

Проблема

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

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

Решение

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

Проблема

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

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

Решение

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

  • Проконтролировать наличие лишних полей, приблизив размеры
  • Проконтролировать отсутствие манипуляций со строками: там, где возможно, заменить на ReadOnlySpan/ReadOnlyMemory
  • Освобождать ссылку как можно раньше
  • Воспользуйтесь пуллингом
  • Кэши и пулы "прогревайте" двойным GC чтобы уплотнить объекты. Тем самым вы избегаете проблем с карточным столом.
Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть