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

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

Wall-clock time

Время работы любого приложения можно разбить на две части:

  • on-cpu — фактическое выполнение процессором кода программы;

  • off-cpu — ожидание различных событий, таких как завершение операций ввода-вывода, доступ к внешним ресурсам, сетевые задержки и так далее.

Вместе эти части представляют собой wall-clock time, то есть общее время, затраченное на выполнение программы. Визуально wall-clock выглядит как чередование сегментов on-cpu и off-cpu.

Как это связано с выбором профилировщика? Дело в том, что при первичном анализе важно смотреть на всю картину целиком для формирования правильных выводов относительно производительности. Поэтому использовать стоит тот инструмент, который покажет как on-cpu-, так и off-cpu-части.

Отличным инструментом для профилирования является perf, который позволяет довольно точно оценить алгоритм работы приложения без серьезного влияния на производительность. Но у него есть свои особенности:

  • в сборку приложения необходимо добавить флаг -fno-omit-frame-pointer, иначе снимки стека будут содержать мало полезной информации. Чем больше зависимостей и компонентов будет собрано с этим флагом, тем точнее perf сформирует данные;

  • perf не покажет off-cpu-часть, которая интересна при первом осмотре.

Поэтому рассмотрим другой инструмент, позволяющий получить wall-clock-профиль — pt-pmp.

Poor man's profiler

pt-pmp — это часть Percona Toolkit, вдохновленная poor man's profiler. Идея заключается в следующем: профилировщик при помощи дебаггера (по умолчанию — gdb) подключается к программе и собирает семпл стека (stack trace) для всех потоков (в том числе находящихся в состоянии off-cpu). Эти операции повторяются несколько раз с определенным интервалом, а затем полученные снимки агрегируются, формируя wall-clock-профиль тестируемой программы.

На основе полученного профиля можно построить флеймграф — это специальное представление stack trace профилируемого приложения для определения горячих путей в коде (Percona описала алгоритм получения флеймграфов на базе pt-pmp в своей статье).

В качестве демонстрации возможностей pt-pmp воспользуемся open-source-приложением Stockfish. Это шахматный движок для определения оптимальных ходов. Команды для сборки профиля и постройки графа:

  • Загрузка pt-pmp и FlameGraph.

git clone https://github.com/brendangregg/FlameGraph

git clone https://github.com/percona/percona-toolkit.git

  • Запуск бенчмарка stockfish в многопоточном режиме с длительностью теста в 40 секунд. Описание флагов и другие режимы можно посмотреть на GitHub.

./stockfish bench 64 4 40000 current movetime >"stockfish.stdout" 2>"stockfish.stderr" &

  • Запуск pt-pmp, флаг iteration указывает на количество собираемых семплов.

sudo ./percona-toolkit/bin/pt-pmp -p $(pidof stockfish) --iterations=100 > pmp.stacktrace 2>pmp.stderr

  • Обработка полученного профиля, она необходима для корректной работы flamegraph.pl:

    • удаление даты, которую профилировщик вставляет на первую линию файла;

    • перестановка местами колонок с количеством потоков и backtrace;

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

tail -n+2 pmp.stacktrace | awk '{print $2, $1}' | sed -e 's/,/;/g' > pmp.profile

  • Создание флеймграфа.

./FlameGraph/flamegraph.pl --title "stockfish PMP flamegraph" pmp.profile > flamegraph_pmp.svg

Получился следующий результат.

Граф действительно отображает wall-clock-профиль: два столбца слева иллюстрируют on-cpu-часть, самый правый столбец с методом Stockfish::Thread::wait_for_search_finished представляет собой off-cpu-часть. Тем не менее pt-pmp обладает следующими недостатками:

  • Невысокая точность измерений. По сравнению с perf, pt-pmp собирает гораздо меньше семплов, и это может быть критично, когда происходит работа над оптимизацией в 1–2%: проблемные методы могут просто скрываться от глаз или при сравнении двух флеймграфов до и после оптимизации количество семплов, которые относятся к определенному методу, может не измениться, хотя ожидается обратное.

  • Использование gdb влияет на производительность тестируемого приложения, так как при снятии семпла работа модуля приостанавливается на некоторое время, и количество таких остановок прямо пропорционально количеству итераций. Если сравнивать результаты бенчмарка stockfish в простом запуске и при использовании pt-pmp, то видно, что производительность упала практически на 100%.

Nodes searched

Nodes/second

Разница в процентах

Обычный запуск

108379841

2709428

97,5%

Запуск с pt-pmp(gdb) 

50614339

1012266

98%

  • pt-pmp может обрезать название метода при агрегировании снимков стека в рамках функции aggregate_stacktrace: в этом примере большинство методов отмечены как int, что дает мало информации.

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

Делаем бедного человека богаче

Начнем с использования gdb. Этот отладчик, который используется по умолчанию в рамках pt-pmp, необходим для того, чтобы собрать stack trace, но он довольно тяжелый и ощутимо влияет на производительность. Поэтому логичным будет использование более легковесного инструмента. Начиная с версии percona-toolkit 3.5.8 (сейчас она находится в разработке, нужные правки находятся в ветке 3.x) pt-pmp позволяет выбрать инструмент для сборки снимков стека. Доступны следующие варианты:

  • gdb;

  • eu-stack — легковесный инструмент из пакета elfutils, с помощью которого можно получить снимок стека каждого потока программы;

  • pt-eustack-resolver — обертка над eu-stack, которая дополнительно форматирует снимки стека аналогично выводу gdb thread apply all bt.

Для наших целей подойдет обычный eu-stack. Для его использования нужно передать дополнительный параметр dumper при запуске pt-pmp:

sudo ./percona-toolkit/bin/pt-pmp --dumper=eu -p $(pidof stockfish) --iterations=100 > pmp.stacktrace 2>pmp.stderr

При таком подходе флеймграф выглядит так.

Результат получился похожим на gdb-версию, однако производительность stockfhish заметно улучшилась: бенчмарк стал выдавать достаточно близкие к обычному запуску результаты.

Nodes searched

Nodes/second

Разница в процентах

Обычный запуск

108379841

2709428

0,82%

Запуск с pt-pmp (eu-stack)

107494533

2687296

0,82%

Также заметно ускорился процесс работы pt-pmp: версия с gdb для 100 итераций отрабатывала примерно за 30 секунд, в случае с eu-stack время сбора приблизилось к 6 секундам.

Чтобы метод aggregate_stacktrace выводил полные имена функций, нужно отредактировать код pt-pmp следующим образом:

diff --git a/bin/pt-pmp b/bin/pt-pmp
index 0aacb473..7575a576 100755
--- a/bin/pt-pmp
+++ b/bin/pt-pmp
@@ -604,8 +604,15 @@ aggregate_stacktrace() {
          if ( ${maxlen:-0} == 0 || c < ${maxlen:-0} ) {
             if ( \$2 ~ /0x/ ) {
                if (dumper ~ /^eu/) {
-                  targ = \$3;
-                  tfile= \$NF;
+                  if ( NF > 2 ) 
+                     \$1 = \$2 = \"\";
+                     OFS=\"\";
+                     targ = \$0
+                  }
+                  else {
+                     targ = \$3;
+                     tfile= \$NF;
+                  }
                }else {
                   if ( \$4 ~/void|const/ ) {

Еще раз формируем флеймграф обновленным pt-pmp.

Результат выглядит гораздо информативнее, чем изначальная версия (теперь видны полные названия методов), on-cpu- и off-cpu-части присутствуют.

Как повысить точность измерений

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

Для сравнения прогоним тот же бенчмарк, но флеймграф соберем через perf.

  • Запуск perf record для получения снимков стека длительностью в 6 секунд (бенчмарк уже должен быть запущен).

sudo perf record -F 99 -g -p "$(pidof stockfish)" -- sleep 6

  • Обработка полученных данных для совместимости с flamegraph.pl.

sudo perf script | ./FlameGraph/stackcollapse-perf.pl > perf.profile

  • Формирование флеймграфа.

./FlameGraph/flamegraph.pl --title "stockfish perf flamegraph" perf.profile > flamegraph_perf.svg

Для Stockfish::Thread::idle_loop за 6 секунд perf собрал примерно 34 000 000 000 семплов, что гораздо больше, чем у pt-pmp. Такая точность позволяет увидеть функции, которые pt-pmp может просто не показать: например, perf-флеймграф отображает более глубокую вложенность метода Stockfish::Search::Worker::search (это можно понять по «гребню волны»).

Производительность бенчмарка изменилась примерно на 2% при использовании perf, что довольно близко к показателям при pt-pmp.

Nodes searched

Nodes/second

Разница в процентах

Обычный запуск

108379841

2709428

1,96%

Запуск с perf

106258084

2656385

1,96%

Итоговый алгоритм профилирования

Какой же инструмент выбрать для начального и последующих осмотров? Начать c pt-pmp, а потом переключиться на perf? Важно помнить, что, когда появляется такой вопрос, всегда есть вариант не выбирать вовсе. Действительно, pt-pmp позволяет определить места, в которых приложение простаивает, а perf полезен в локальных оптимизациях, где необходима повышенная точность измерений. К тому же с помощью perf можно проводить Top-Down-анализ, оценивая различные метрики работы процессора. Так почему бы не объединить эти два инструмента?

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

Алгоритм имеет следующий вид:

  • сборка тестового приложения с флагом -fno-omit-frame-pointer и его запуск в фоновом режиме;

  • запуск perf для получения точного профиля с ожиданием завершения работы;

  • запуск pt-pmp для получения wall-clock-профиля;

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

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

Заключение

При помощи описанного алгоритма удалось найти множество оптимизаций, которые улучшили производительность продуктов Positive Technologies. Эти инструменты довольно просты в использовании и не оказывают серьезного влияния на работу приложения, что позволяет интегрировать их в нагрузочные тесты для сбора информации о программе. Однако появляется логичный вопрос: что дальше? Ведь использование двух инструментов профилирования выглядит избыточным, хотелось бы иметь какую-то одну утилиту, позволяющую получить точный wall-clock-профиль. Пока что каких-то готовых альтернатив нет. Довольно много работ ведется в направлении разворачивания стека без использования frame pointers, например, Polar Signals в своей статье описали этот механизм на базе DWARF с использованием eBPF, но тут присутствует все та же проблема с off-cpu-частью, которую инструмент не показывает. Поэтому нам остается следить за новостями в сфере инструментов профилирования и пользоваться Poor Man's Profile, который по-прежнему актуален. Пишите в комментариях, какие инструменты для анализа вы применяете в своих проектах.

Один из продуктов, производительность которого мы постоянно улучшаем — MaxPatrol SIEM. Узнать больше о новых фичах продукта можно 27 июня в 14:00 на онлайн-запуске MaxPatrol SIEM 8.2. Регистрируйтесь и задавайте свои вопросы. Авторов самых лучших мы обязательно наградим.


Александр Слепнев

Специалист отдела инжиниринга производительности, Positive Technologies

Комментарии (1)


  1. leitosama
    10.06.2024 11:26
    +2

    При помощи описанного алгоритма удалось найти множество оптимизаций, которые улучшили производительность продуктов Positive Technologies.

    Хотелось бы увидеть какие-то приближенные к реальности примеры находок. Допустим вот Вы запустили инструмент и что было дальше? Как увидели проблему, как нашли итоговый кусочек кода, который надо оптимизировать.

    Считаю что статья заслуживает второй практической части.