GoSuda

Возможность выпить чашку зелёного чая, которую у нас отнимет среда выполнения Go: GreenTea GC

By snowmerak views ...

Зачем нужен ещё один новый GC

Почему существующий GC устроен именно так

Текущий GC в Go можно описать следующими характеристиками.

  • concurrent tri-color mark and sweep
    • Используются три цвета (белый, серый, чёрный) для отслеживания состояния объектов.
      • Белый: объект, который ещё не был посещён
      • Серый: объект, который был посещён, но его дочерние объекты посещены не все
      • Чёрный: объект, все дочерние объекты которого были посещены
      • После завершения обхода все белые объекты собираются.
    • Write Barrier используется для защиты памяти, создаваемой во время работы GC.
      • Время включения и выключения Write Barrier составляет значительную часть того, что обычно называют Stop-The-World (далее STW).
    • Этот процесс выполняется конкурентно.
  • No Compaction
    • GC в Go не выполняет компактификацию.
    • Может возникать фрагментация памяти.
      • Однако для объектов размером до 32 КБ используется per-P кэш аллокатора памяти Go, что позволяет минимизировать фрагментацию.
  • Non-Generational
    • GC в Go не управляет объектами по поколениям.
    • Все объекты принадлежат одному поколению.
  • Escape Analysis
    • Go использует Escape Analysis, чтобы определить, выделяется ли объект в куче или на стеке.
    • Приблизительно можно считать, что если возникают висячие указатели или используются интерфейсы, объект выделяется в куче.

Что здесь важно

GC в Go обходит все объекты, начиная с корней, и выполняет трёхцветную разметку. Этот процесс можно кратко описать как конкурентный обход графа с топологической сортировкой. Однако каждый объект, скорее всего, находится в разных областях памяти. Если говорить прямо:

  1. Допустим, есть две области памяти, расстояние между которыми составляет около 32 МБ.
  2. В каждой из этих областей выделено по 5 объектов.
    1. Объекты A, B и C находятся в области 1
    2. Объекты D и E находятся в области 2
  3. Порядок ссылок между объектами: A -> D -> B -> E -> C.
  4. Когда запускается GC, он последовательно посещает A, D, B, E и C.
  5. При этом A и D находятся в разных областях памяти, поэтому при переходе от посещения A к посещению D необходимо переключаться между областями памяти.
  6. В этом процессе возникают так называемые издержки перехода по памяти: переключение области памяти, заполнение новых строк кэша и т. д. Значительная часть процессорного времени приложения может уходить на эту работу GC.
  7. Эти издержки возникают на протяжении всего времени работы GC.

В чём же проблема?

Такое поведение GC становится серьёзным ограничением в следующих случаях.

  • Большое количество ядер и большой объём памяти
    • GC в Go по умолчанию работает конкурентно.
    • Однако если область памяти обширна, а объекты распределены, несколько ядер одновременно обходят объекты в разных участках памяти.
    • В этом случае, если шина между CPU и памятью недостаточно широка, пропускная способность памяти может стать узким местом.
    • Кроме того, из-за физического расстояния каждый переход при обходе может вносить относительно большую задержку.
  • Большое количество мелких объектов
    • Если вы используете Go для работы с глубокими деревьями или графами, либо с узлами, имеющими много потомков, GC должен обойти все эти объекты.
    • Если при этом объекты распределены по разным областям памяти, задержка возникает по первой причине.
    • Кроме того, при большом количестве мелких объектов GC должен обойти их все, поэтому во время работы GC значительная часть ресурсов ядер CPU уходит на его работу.

Для решения этих проблем команда Golang представила GreenTea GC.

Больше нет времени на зелёный чай

Что отнимает зелёный чай

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

Возможно, поэтому команда Golang выбрала подход на основе мемory span (Memory Span).

Что такое Memory Span?

Memory span — это область, в которой выделяется относительно большой участок памяти для размещения мелких объектов. Новый GC под названием GreenTea GC выполняет сборку мусора именно над такими memory span. Детали работы в целом почти совпадают с традиционным Tri-Color Mark and Sweep.

Работа GreenTea GC

Сначала GreenTea GC выделяет memory span. Его размер, как было сказано ранее, составляет 8 КиБ, что достаточно велико. Внутри него можно разместить объекты размером до 512 байт. Размер узлов деревьев или графов из приведённых примеров, а также структур, которые обычно попадают в кучу, вряд ли превышает этот предел. Объекты размером до 512 байт при каждом попадании в кучу накапливаются в этом memory span, и когда он заполняется, выделяется новый memory span.

Когда происходит GC, GreenTea GC ставит эти memory span в очередь и проверяет их последовательно. При этом GreenTea GC использует планирование, почти аналогичное существующей модели GMP. Реализованы и такие механизмы, как кража работы. В любом случае, рабочий поток, извлекший memory span из очереди, проверяет объекты внутри назначенного ему memory span. На этом этапе используется тот же Tri-Color Mark and Sweep.

Какие здесь преимущества?

По сути, по сравнению с существующим GC различие только одно: единицей выполнения GC становится не объект, а memory span. Благодаря этому GC получает следующие преимущества.

  • Локальность памяти: поскольку объекты собраны внутри memory span, переходы по памяти при обходе объектов минимизированы. То есть можно максимально эффективно использовать кэш CPU.
  • Повышение производительности GC: выполняя GC по единицам memory span, GC работает эффективнее в многоядерной среде.
  • Оптимизация выделения памяти: memory span выделяются фиксированного размера, поэтому накладные расходы на выделение мелких объектов уменьшаются, а вероятность фрагментации снижается. Это повышает эффективность освобождения памяти.

Короче говоря, теперь мы можем чаще и с меньшими опасениями выделять объекты размером до 512 байт.

Однако и здесь есть определённые благоприятные случаи.

  • Структуры деревьев и графов: высокий fan-out и редкие изменения
    • B+ деревья, Trie, AST (Abstract Syntax Tree)
    • Структуры данных, дружелюбные к кэшу
  • Пакетная обработка: рабочие процессы с массовым выделением мелких данных
    • Множество мелких объектов, создаваемых при парсинге JSON
    • DAO-объекты из результатов запросов к базе данных

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

Итак

Команда Golang, вероятно, намерена и в дальнейшем постепенно улучшать GC. GreenTea GC можно рассматривать как небольшое изменение в узкой области, и он не заменяет существующий GC. Однако GreenTea GC интересен как пример того, какие проблемы стоят перед командой Golang или какие они ожидают. Задача оказалась неочевидно сложной, но попытка решить её добавлением относительно простой концепции тоже произвела впечатление. Лично я считаю это интересным случаем.

GreenTea GC представлен как экспериментальная функция, которая появится начиная с Go 1.25. Её можно включить с помощью переменной окружения GOEXPERIMENT=greenteagc. Поскольку эта функция пока экспериментальная, перед использованием в производственной среде необходимо провести тщательное тестирование.

Ссылки