Оптимизация программ под 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
    • 8.4k
    • 4
    Семинары Станислава Сидристого
    68.00
    CLRium #6: Concurrency & Parallelism
    Support the author
    Share post

    Comments 4

      –1
      Карточный стол — это вы card table так перевели?
        0
        Нисколько не умаляю важности и крутости такого рода знаний, однако это немного похоже на то «Как преодолеть трудности которые вы сами себе создали», т.е. выбираем язык со сборкой мусора и потом пытаемся оптимизировать программу для этой не очень прозрачной и сложной штуки. Но я вполне могу понять что есть много случает когда это ок.
          +1
          А отсутствие сборки мусора приводит к ручному выделению памяти на разных хипах — чтобы преодолеть фрагментацию и дальнейшее освобождение памяти вручную… Я пока что не встречал больших сложностей с .NET, зато много их видел в том же C++
            0
            Не вполне понятно. В С++ вы можете реализовать ту стратегию выделения, которая нужна соответствует патерну использования и не пробиваться через логику gc.

            Например http-сервис имеет буфер (арену) для каждого запроса. Обработчик использует ее для своих нужд и просто забывает про нее после окончания запроса — память переходит следующему запросу. Получается стратегия выделения динамической памяти, которая не требует обращений к malloc/free в пределах запроса (очень простые операция выделения/освобождения).

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

        Only users with full accounts can post comments. Log in, please.