Погружение в новую экспериментальную функцию Go

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

Подробнее см. в этом обсуждении на GitHub.

Введение

Go 1.20 представляет экспериментальную концепцию «арен» для управления памятью, которую можно использовать для повышения производительности ваших программ Go. В этом сообщении блога мы рассмотрим:

  • Что такое арены
  • Как они работают
  • Как определить, могут ли ваши программы извлечь выгоду из использования арен?
  • Как мы использовали арены для оптимизации одного из наших сервисов

Что такое арены памяти?

Go — это язык программирования, использующий сборку мусора, что означает, что среда выполнения автоматически управляет выделением и освобождением памяти для программиста. Это устраняет необходимость в ручном управлении памятью, но за это приходится платить:

Среда выполнения Go должна отслеживать каждый выделенный объект, что приводит к повышению производительности.

В некоторых сценариях, например, когда HTTP-сервер обрабатывает запросы с большими BLOB-объектами protobuf (которые содержат множество мелких объектов), это может привести к тому, что среда выполнения Go будет тратить значительное количество времени на отслеживание каждого из этих отдельных выделений, а затем освобождает их. В результате это также вызывает значительные накладные расходы на производительность.

Арены предлагают решение этой проблемы, уменьшая накладные расходы, связанные с множеством небольших распределений. В этом примере большого двоичного объекта protobuf большой кусок памяти (арена) может быть выделен перед синтаксическим анализом, что позволяет затем разместить все проанализированные объекты в арене и отслеживать их как коллективную единицу.

После завершения синтаксического анализа вся арена может быть освобождена сразу, что еще больше снижает накладные расходы на освобождение множества мелких объектов.

Идентификация кода, который может принести пользу аренам

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

С помощью Pyroscope нам удалось получить профиль распределения (alloc_objects) одного из наших облачных сервисов:

Вы можете видеть, что большинство распределений (533.30 M) происходит из одной области кода — это фиолетовый узел внизу, где он вызывает функцию InsertStackA. Учитывая, что он составляет 65% выделений, это хороший кандидат на использование арен. Но достаточно ли выигрыша в производительности можно добиться, сократив эти выделения? Давайте посмотрим на профиль процессора (cpu) того же сервиса:

Несколько вещей выделяются:

  • Программа тратит много процессорного времени на одну и ту же функцию InsertStackA, поэтому определенно есть потенциал для значительного прироста производительности.
  • Если вы ищете runtime.mallocgc (несколько розовых узлов внизу), вы увидите, что эта функция часто вызывается в разных местах и ​​занимает около 14% нашего общего времени выполнения.
  • Около 5% процессорного времени тратится на runtime.gcBgMarkWorker (розовые узлы, расположенные в правой части пламенного графика)

Так что теоретически, если бы мы оптимизировали все выделения в этой программе, мы могли бы сократить примерно 14% + 5% = 19% процессорного времени.

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

Оптимизации, которые мы сделали

Если вы хотите продолжить, есть общедоступный запрос на включение в репозиторий Pyroscope, который вы можете использовать в качестве справочного материала.

  • Для начала мы создали компонент-оболочку, который отвечает за распределение слайсов или структур. Если арены включены, этот компонент выделяет слайсы с помощью арены, в противном случае он использует стандартную функцию make. Мы делаем это с помощью тегов сборки (//go:build goexperiment.arenas). Это позволяет легко переключаться между распределением арены и стандартным распределением во время строительства.
  • Затем мы добавили вызовы инициализация и очистка для арен вокруг кода парсера.
  • После этого мы заменили обычные вызовы make вызовами make из нашего компонента-обертки
  • Наконец, мы создали пироскоп с включенными аренами и постепенно развернули его в нашей производственной среде Pyroscope Cloud.

Результаты наших экспериментов с аренами

График выше представляет собой профиль после того, как мы внедрили изменения. Вы можете видеть, что многие из вызовов runtime.mallocgc теперь убраны, но заменены эквивалентом для конкретной арены (runtime.(*userArena).alloc), вы также можете видеть, что накладные расходы на сборку мусора сокращены вдвое.

Трудно увидеть точную сумму экономии только от просмотра графиков пламени, но, взглянув на нашу панель инструментов Grafana, которая объединяет наш график пламени с использованием ЦП из показателей AWS, мы увидели снижение использования ЦП примерно на 8%. Это напрямую приводит к экономии 8 % на счетах за облачные услуги для этой конкретной услуги, что делает улучшение стоящим.

Это может показаться не таким уж большим, но важно отметить, что это сервис, который уже немного оптимизирован. Например, синтаксический анализатор protobuf, который мы используем, вообще не выделяет никакой дополнительной памяти, накладные расходы на сборку мусора (5%) также находятся в нижней части спектра для наших сервисов. Мы думаем, что есть еще много возможностей для улучшения в других частях кодовой базы, поэтому мы рады продолжить наши эксперименты с аренами.

Компромиссы и недостатки

Хотя арены могут обеспечить преимущества в производительности, важно учитывать компромиссы перед их использованием. Основной недостаток использования арен заключается в том, что при использовании арен теперь приходится управлять памятью вручную, и если вы не будете осторожны, это может привести к серьезным проблемам:

  • Неправильное освобождение памяти может привести к утечке памяти.
  • Попытка доступа к объектам с ранее освобожденной арены может привести к сбою программы.

Вот наши рекомендации:

  • Используйте арены только в критических путях кода. Не используйте их везде
  • Профилируйте свой код до и после использования арен, чтобы убедиться, что вы добавляете арены в тех областях, где они могут принести наибольшую пользу.
  • Обратите особое внимание на жизненный цикл объектов, созданных на арене. Убедитесь, что вы не передаете их другим компонентам вашей программы, где объекты могут пережить арену.
  • Используйте defer a.Free(), чтобы убедиться, что вы не забыли освободить память
  • Используйте arena.Clone() для клонирования объектов обратно в кучу, если вы хотите использовать их после освобождения арены.

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

Решение проблем сообщества

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

Большая часть критики обоснована, но направлена ​​не по адресу. Мы не ожидаем широкого распространения арен. Мы рассматриваем арены как мощный инструмент, но подходящий только для определенных ситуаций. На наш взгляд, арены должны быть включены в стандартную библиотеку, однако их использование не должно поощряться, как и использование unsafe, reflect или cgo.

Наш опыт работы с аренами был очень положительным, и мы смогли показать, что арены могут значительно сократить время, затрачиваемое на сборку мусора и выделение памяти. Эксперимент, описанный в этой статье, был сосредоточен на одном, уже высоко оптимизированном сервисе, и нам все же удалось выжать 8% дополнительной производительности за счет использования арен. Мы думаем, что многие пользователи могли бы получить гораздо больше пользы от использования арен в своих кодовых базах.

В дополнение к этому мы также обнаружили, что арены легче реализовать по сравнению с другими оптимизациями, которые мы пробовали в прошлом, такими как использование пулов буферов или написание пользовательских парсеров protobuf без выделения памяти. И по сравнению с другими типами оптимизации они имеют те же недостатки, но дают больше преимуществ — поэтому, на наш взгляд, арены — это чистая победа. Мы надеемся, что в будущем арены станут частью стандартной библиотеки (и частью часто используемых пакетов, таких как protobuf или парсеры JSON).

Заключение

Арены — это мощный инструмент для оптимизации программ Go, особенно в сценариях, когда ваши программы тратят значительное количество времени на синтаксический анализ больших больших двоичных объектов protobuf или JSON. У них есть потенциал для значительного повышения производительности, но важно отметить, что это экспериментальная функция, и нет никаких гарантий совместимости или дальнейшего существования в будущих выпусках.

Мы рекомендуем вам профилировать ваши приложения и попробовать использовать арены в ограниченной части вашей кодовой базы и сообщить о своих выводах рабочей команде.