Оптимизация с использованием профилей

Начиная с Go 1.20, компилятор Go поддерживает оптимизацию с использованием профилей (PGO), чтобы дополнительно улучшить сборку.

Содержание:

Обзор
Сбор профилей
Сборка с использованием PGO
Примечания
Часто задаваемые вопросы
Приложение: альтернативные источники профилей

Обзор

Оптимизация с использованием профилей (PGO), также известная как оптимизация с обратной связью (FDO), — это техника оптимизации компилятора, при которой информация (профиль) из репрезентативных запусков приложения передаётся обратно в компилятор для следующей сборки приложения. Эта информация используется для принятия более обоснованных решений при оптимизации. Например, компилятор может решить более агрессивно встраивать функции, которые профиль указывает как часто вызываемые.

В Go компилятор использует CPU pprof профили в качестве входных данных для PGO, такие как из пакетов runtime/pprof или net/http/pprof.

На момент Go 1.22, бенчмарки для репрезентативного набора программ на Go показывают, что сборка с использованием PGO повышает производительность примерно на 2–14%. Ожидается, что усиление производительности будет расти со временем, поскольку дополнительные оптимизации будут использовать PGO в будущих версиях Go.

Сбор профилей

Компилятор Go ожидает CPU pprof профиль в качестве входных данных для PGO. Профили, сгенерированные средой выполнения Go (например, из runtime/pprof и net/http/pprof), могут использоваться напрямую в качестве входных данных для компилятора. Также возможно использование или преобразование профилей из других систем профилирования. См. приложение для дополнительной информации.

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

Типичный рабочий процесс выглядит следующим образом:

  1. Собрать и выпустить начальный бинарный файл (без PGO).
  2. Собрать профили из рабочей среды.
  3. Когда наступит время выпускать обновлённый бинарный файл, собрать его из последнего исходного кода и передать профиль из рабочей среды.
  4. Повторить шаг 2

Go PGO в целом устойчиво к дисбалансу между профилируемой версией приложения и версией, собираемой с профилем, а также к сборке с профилями, собранными из уже оптимизированных бинарных файлов. Это то, что делает возможным итеративный жизненный цикл. См. раздел AutoFDO для дополнительных сведений о данном рабочем процессе.

Если собрать профиль в среде production (например, командная строка, распространяемая конечным пользователям) сложно или невозможно, можно собирать профиль с использованием репрезентативного бенчмарка. Обратите внимание, что создание репрезентативных бенчмарков часто является довольно сложной задачей (как и поддержание их репрезентативности при изменении приложения). В частности, микробенчмарки обычно не являются хорошими кандидатами для профилирования PGO, поскольку они охватывают лишь небольшую часть приложения, что приводит к незначительным улучшениям при применении ко всему программному обеспечению.

Сборка с использованием PGO

Стандартный подход к сборке заключается в том, чтобы сохранить профиль CPU pprof с именем файла default.pgo в каталоге основного пакета профилируемого бинарного файла. По умолчанию, go build автоматически обнаруживает файлы default.pgo и включает PGO.

Рекомендуется хранить профили напрямую в репозитории исходного кода, поскольку профили являются входными данными для сборки, важными для воспроизводимой (и производительной!) сборки. Хранение профилей рядом с исходным кодом упрощает процесс сборки, поскольку не требуются дополнительные шаги для получения профиля, кроме как получение исходного кода.

Для более сложных сценариев флаг go build -pgo управляет выбором профиля PGO. По умолчанию этот флаг имеет значение -pgo=auto, что соответствует поведению default.pgo, описанному выше. Установка флага в значение -pgo=off полностью отключает оптимизации PGO.

Если вы не можете использовать default.pgo (например, разные профили для разных сценариев одного бинарного файла, невозможность сохранить профиль вместе с исходным кодом и т. д.), можно напрямую передать путь к используемому профилю (например, go build -pgo=/tmp/foo.pprof).

Примечание: Путь, передаваемый в -pgo, применяется ко всем основным пакетам. Например, go build -pgo=/tmp/foo.pprof ./cmd/foo ./cmd/bar применяет foo.pprof к обоим бинарным файлам foo и bar, что обычно не является желательным результатом. Обычно разные бинарные файлы должны иметь разные профили, которые передаются через отдельные вызовы go build.

Примечание: До Go 1.21 значение по умолчанию — -pgo=off. PGO необходимо явно включать.

Примечания

Сбор репрезентативных профилей из production

Ваша среда production является лучшим источником репрезентативных профилей для вашего приложения, как описано в разделе Сбор профилей.

Простой способ начать работу — добавить net/http/pprof

Добавьте её в своё приложение, а затем получите /debug/pprof/profile?seconds=30 с произвольного экземпляра вашего сервиса. Это отличный способ начать, но существуют способы, при которых это может быть непредставительным:

  • Экземпляр может не выполнять никаких задач в момент профилирования, даже если обычно он занят.

  • Паттерны трафика могут меняться в течение дня, вызывая изменение поведения в течение дня.

  • Экземпляры могут выполнять длительные операции (например, 5 минут выполняют операцию A, затем 5 минут — операцию B и т. д.). Профиль на 30 секунд, скорее всего, будет охватывать только один тип операции.

  • Экземпляры могут не получать равномерное распределение запросов (некоторые экземпляры получают больше запросов одного типа, чем другие).

Более надёжная стратегия — сбор нескольких профилей в разное время с разных экземпляров для ограничения влияния различий между индивидуальными профилями экземпляров. Затем такие профили можно объединить в один профиль для использования с PGO.

Многие организации запускают службы «непрерывного профилирования», которые автоматически выполняют такой вид выборочного профилирования на всей группе экземпляров, что может быть использовано как источник профилей для PGO.

Объединение профилей

Инструмент pprof может объединять несколько профилей следующим образом:

<code>$ go tool pprof -proto a.pprof b.pprof > merged.pprof
</code>

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

AutoFDO

Go PGO спроектирован для поддержки рабочего процесса в стиле «AutoFDO».

Рассмотрим подробнее рабочий процесс, описанный в разделе Сбор профилей:

  1. Сборка и выпуск начального двоичного файла (без PGO).
  2. Сбор профилей из production-среды.
  3. Когда наступает время выпускать обновлённый двоичный файл, сборка из последнего исходного кода с предоставлением production-профиля.
  4. Переход к шагу 2

Это звучит обманчиво просто, но есть несколько важных свойств, которые стоит отметить:

  • Разработка всегда продолжается, поэтому исходный код профилируемой версии двоичного файла (шаг 2) вероятно немного отличается от последнего исходного кода, который собирается (шаг 3). Go PGO спроектирован быть устойчивым к этому, что мы называем стабильностью исходного кода.

  • Это замкнутый цикл. То есть, после первой итерации профилированная версия бинарного файла уже оптимизирована с использованием профиля из предыдущей итерации. Go PGO также спроектирован так, чтобы быть устойчивым к этому, что мы называем итерационной стабильностью.

Стабильность исходного кода достигается с использованием эвристик для сопоставления выборок из профиля с исходным кодом, который компилируется. В результате, множество изменений в исходном коде, таких как добавление новых функций, не оказывают никакого влияния на сопоставление существующего кода. Когда компилятор не может сопоставить изменённый код, некоторые оптимизации теряются, но стоит отметить, что это постепенное ухудшение. Одна функция, не способная быть сопоставлена, может потерять возможности оптимизации, но в целом выгода от PGO обычно распределяется между множеством функций. См. раздел стабильность исходного кода для получения дополнительных сведений о сопоставлении и ухудшении оптимизаций.

Итерационная стабильность — это предотвращение циклических колебаний производительности при последовательных сборках с использованием PGO (например, сборка #1 — быстрая, сборка #2 — медленная, сборка #3 — быстрая и т. д.). Мы используем CPU-профили для выявления горячих функций, на которые следует направлять оптимизации. В теории, горячая функция может быть настолько ускорена с помощью PGO, что перестанет быть горячей в следующем профиле и не будет оптимизирована, что приведёт к замедлению. Компилятор Go применяет консервативный подход к оптимизациям PGO, что, по нашему мнению, предотвращает значительные колебания. Если вы наблюдаете подобную нестабильность, пожалуйста, создайте задачу на go.dev/issue/new.

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

Стабильность исходного кода и рефакторинг

Как описано выше, Go PGO делает все возможное, чтобы продолжать сопоставлять выборки из старых профилей с текущим исходным кодом. Конкретно, Go использует смещения строк внутри функций (например, вызов на 5-й строке функции foo).

Множество распространённых изменений не нарушают сопоставление, включая:

  • Изменения в файле вне горячей функции (добавление/изменение кода выше или ниже функции).

  • Перемещение функции в другой файл внутри того же пакета (компилятор вообще игнорирует имена файлов исходного кода).

Некоторые изменения могут нарушить сопоставление:

  • Изменения внутри горячей функции (может повлиять на смещения строк).

  • Переименование функции (и/или типа для методов) (изменяет имя символа).

  • Перемещение функции в другой пакет (изменяет имя символа).

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

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

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

Для рутинных переименований существует теоретическая возможность перезаписать существующий профиль, заменив старые имена символов на новые. github.com/google/pprof/profile содержит примитивы, необходимые для перезаписи профиля pprof таким образом, однако на момент написания статьи не существует готового инструмента для этого.

Производительность нового кода

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

Часто задаваемые вопросы

Возможно ли применить PGO к пакетам стандартной библиотеки Go?

Да. PGO в Go применяется ко всему программному обеспечению. Все пакеты пересобираются с учетом потенциальных профильных оптимизаций, включая пакеты стандартной библиотеки.

Возможно ли применить PGO к пакетам в зависимых модулях?

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

Будет ли программа медленнее с PGO, если профиль нереалистичен?

Нет. Хотя профиль, который не отражает поведение в продакшене, может привести к оптимизациям в холодных частях приложения, он не должен замедлять горячие части приложения. Если вы столкнетесь с программой, где PGO приводит к худшей производительности, чем при отключении PGO, пожалуйста, создайте отчет на go.dev/issue/new.

Можно ли использовать один профиль для сборок с разными GOOS/GOARCH?

Да. Формат профилей одинаков для всех конфигураций ОС и архитектур, поэтому они могут использоваться в различных конфигурациях. Например, профиль, собранный из двоичного файла linux/arm64, может быть использован в сборке windows/amd64.

Тем не менее, оговорки по стабильности исходного кода, обсуждавшиеся выше, остаются актуальными.

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

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

В качестве конкретного примера, внутренняя реализация обработки файлов в пакете os отличается между Linux и Windows. Если эти функции являются горячими в профиле для Linux, то эквиваленты для Windows не будут получать оптимизации PGO, потому что они не соответствуют профилям.

Вы можете объединять профили сборок с различными GOOS/GOARCH. См. следующий вопрос о компромиссах, связанных с таким объединением.

Как следует обрабатывать один бинарный файл, используемый для разных типов рабочих нагрузок?

Здесь нет очевидного выбора. Один бинарный файл, используемый для различных типов рабочих нагрузок (например, база данных, используемая в одном сервисе в режиме чтения, а в другом — в режиме записи), может иметь разные горячие компоненты, которые выигрывают от разных оптимизаций.

Существует три варианта:

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

  2. Собрать один бинарный файл, используя только профили от «самой важной» рабочей нагрузки: выбрать «самую важную» рабочую нагрузку (с наибольшим объемом, наиболее чувствительную к производительности), и собрать с использованием профилей только от этой нагрузки. Это обеспечит лучшую производительность для выбранной нагрузки, и, вероятно, все еще даст незначительные улучшения производительности для других нагрузок благодаря оптимизациям общего кода, разделяемого между нагрузками.

  3. Объединить профили между рабочими нагрузками: взять профили от каждой рабочей нагрузки (с весом, пропорциональным общему объему), и объединить их в один «флото-широкий» профиль, используемый для создания единого общего профиля, который будет использоваться при сборке. Это, вероятно, обеспечит незначительное улучшение производительности для всех рабочих нагрузок.

Как PGO влияет на время сборки?

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

Если вы наблюдаете экстремальное увеличение времени сборки, пожалуйста, создайте заявку на go.dev/issue/new.

Как PGO влияет на размер бинарного файла?

PGO может привести к небольшому увеличению размера бинарного файла из-за дополнительной вставки функций (inlining).

Приложение: альтернативные источники профилей

Профили процессора, созданные средой выполнения Go (через runtime/pprof и т. д.), уже находятся в правильном формате для прямого использования в качестве входных данных для PGO. Однако организации могут использовать альтернативные предпочтительные инструменты (например, Linux perf), или существующие системы непрерывного профилирования на всей флотилии, которые они хотят использовать совместно с Go PGO.

Профили из альтернативных источников могут быть использованы с Go PGO, если они будут преобразованы в формат pprof, при условии, что они соответствуют следующим общим требованиям:

  • Один из индексов выборки должен иметь тип/единицу измерения «samples»/«count» или «cpu»/«nanoseconds».

  • Выборки должны представлять собой выборки времени процессора в месте выборки.

  • Профиль должен быть символизирован (Function.name должен быть задан).

  • Выборки должны содержать кадры стека для встроенных функций. Если встроенные функции опущены, Go не сможет обеспечить итерационную стабильность.

  • Function.start_line должен быть задан. Это номер строки начала функции. То есть строка, содержащая ключевое слово func. Компилятор Go использует это поле для вычисления смещений строк выборок (Location.Line.line - Function.start_line). Обратите внимание, что многие существующие конвертеры pprof опускают это поле.

Примечание: До версии Go 1.21 метаданные DWARF не содержат номера строк начала функций (DW_AT_decl_line), что может затруднить определение строки начала инструментами.

См. страницу PGO Tools вики Go для дополнительной информации о совместимости конкретных сторонних инструментов с PGO.

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang