Когда приложение начинает тормозить без видимых причин, первый инстинкт разработчика - добавить логирование, расставить счётчики, поднять strace. Но всё это работает на поверхности. Если проблема живёт глубже - в планировщике, в сетевом стеке, в системных вызовах или в промахах кэша процессора - поверхностные инструменты дадут только симптомы, но не причину. Именно здесь начинается территория perf.
perf, или perf_events, появился в ядре Linux начиная с версии 2.6.31 в 2009 году. За прошедшие годы он превратился из узкоспециализированного инструмента для работы с PMU-счётчиками в полноценную систему наблюдения за работой ядра и пользовательских программ. Его главное свойство - минимальный overhead при максимальной глубине анализа. Никаких daemon-процессов, никаких дополнительных модулей ядра: один системный вызов, файловый дескриптор и отображённая в память область - вот вся инфраструктура.
Что perf умеет измерять и откуда берёт данные
Сила perf в том, что он работает с несколькими принципиально разными источниками данных одновременно. Понимание этих источников - первый шаг к осмысленному использованию инструмента.
Аппаратные счётчики - это регистры PMU (Performance Monitoring Unit) в процессоре. Они считают реальные события на уровне железа: выполненные инструкции, циклы процессора, промахи кэша L1 и L2, неверно предсказанные ветвления. Эти данные не приблизительные и не интерполированные - это точные цифры от самого CPU. Программные счётчики работают иначе: это события ядра Linux, такие как переключения контекста, minor page faults, миграции задач между ядрами.
Tracepoints - статические точки трассировки, вкомпилированные в ядро в стратегически важных местах: системные вызовы, события TCP/IP, операции файловой системы, планировщик. Они включаются по требованию и несут пренебрежимо малый overhead в выключенном состоянии. Наконец, динамические пробы kprobes и uprobes позволяют встроить точку измерения в любую функцию ядра или пользовательского пространства без перекомпиляции.
Установка perf на Ubuntu/Debian:
apt install linux-tools-common linux-tools-$(uname -r)
Проверить доступные события на конкретной машине:
perf list
perf list | grep cache
perf list | grep sched
Быстрый анализ системы через perf stat и perf top
Разбор производительности удобно начинать с двух команд. perf stat запускает процесс и собирает статистику аппаратных счётчиков за время его работы. perf top работает как top, но показывает горячие функции в реальном времени.
# Базовая статистика для команды
perf stat ls -la /usr
# Расширенная статистика с конкретными событиями
perf stat -e cycles,instructions,cache-misses,cache-references \
./my_program
# Статистика для уже запущенного процесса по PID
perf stat -p 1234 sleep 10
Вывод perf stat покажет, сколько инструкций приходится на каждый цикл (IPC - Instructions Per Cycle). Значение IPC ниже 1.0 на современном суперскалярном процессоре сигнализирует о том, что программа тратит время на ожидание: памяти, блокировок, системных вызовов. Значение выше 2.0 говорит о хорошем использовании конвейера.
# Просмотр горячих функций в реальном времени
perf top
# Только события промахов кэша L1
perf top -e l1-dcache-load-misses
# Только ядро, без пользовательского пространства
perf top --kallsyms=/proc/kallsyms -K
В интерактивном режиме perf top функции отсортированы по доле времени CPU. Маркер [k] рядом с функцией означает kernel space, [.] - user space. Если большинство горячих функций помечены [k], приложение проводит неожиданно много времени в ядре - это повод изучить причину.
Запись и анализ профиля через perf record и perf report
Полноценное профилирование строится на паре команд: perf record собирает данные в файл perf.data, perf report отображает результат с сортировкой и фильтрацией.
# Записать профиль запущенной программы (99 Гц, с трассировкой стека)
perf record -F 99 -g ./my_program
# Записать профиль системы целиком в течение 30 секунд
perf record -F 99 -ag -- sleep 30
# Записать профиль конкретного процесса
perf record -F 99 -g -p 1234 -- sleep 10
Флаг -F 99 задаёт частоту сэмплирования 99 Гц. Почему 99, а не 100? Простое число снижает риск попасть в резонанс с периодическими событиями в самом ядре, которые кратны 100 Гц. Флаг -g включает запись стека вызовов - без него можно увидеть только горячие функции, но не понять, как программа до них добралась.
После записи анализ профиля:
# Интерактивный отчёт
perf report
# Отчёт с разворачиванием стека вызовов
perf report --call-graph=graph
# Текстовый вывод для скриптов и grep
perf report --stdio | head -50
Для того чтобы perf показывал имена функций, а не шестнадцатеричные адреса, нужны символы отладки. Для системных библиотек они обычно доступны в пакетах -dbgsym. Для ядра символы подключаются через пакет linux-image-$(uname -r)-dbgsym или через CONFIG_KALLSYMS=y при сборке.
Flame Graphs и как превратить профиль в читаемую картину
Интерактивный вывод perf report удобен для детального изучения, но плохо подходит для быстрого понимания общей картины. Flame Graphs - визуализация, предложенная Бренданом Греггом, решают эту проблему. По горизонтальной оси отложена доля времени CPU, по вертикальной - глубина стека вызовов. Широкое плато наверху графика - горячий путь, который тратит больше всего времени.
# Установить FlameGraph скрипты
git clone https://github.com/brendangregg/FlameGraph
# Записать профиль с развёрнутыми стеками
perf record -F 99 -ag -- sleep 30
perf script > perf.out
# Построить flame graph
./FlameGraph/stackcollapse-perf.pl perf.out | \
./FlameGraph/flamegraph.pl > flamegraph.svg
Открыв SVG в браузере, можно кликать по прямоугольникам для zoom-in и сразу видеть, где конкретно тратится время. Это принципиально меняет скорость анализа: вместо чтения таблиц с процентами - интуитивная визуальная картина горячих путей.
kprobes и uprobes для динамической трассировки без перекомпиляции
Если стандартных tracepoints недостаточно, perf позволяет создавать динамические точки измерения в любой функции ядра через kprobes. Это работает путём замены первого байта инструкции точкой прерывания - механизм пугающий на первый взгляд, но надёжный на практике.
# Добавить probe на функцию ядра tcp_sendmsg
perf probe --add tcp_sendmsg
# Посмотреть аргументы функции (требует отладочных символов ядра)
perf probe -V tcp_sendmsg
# Записать события probe
perf record -e probe:tcp_sendmsg -ag -- sleep 10
# Удалить probe после использования
perf probe --del tcp_sendmsg
uprobes работают аналогично, но для функций в пользовательских программах и библиотеках:
# Добавить probe на функцию malloc в libc
perf probe -x /lib/x86_64-linux-gnu/libc.so.6 --add malloc
# Трассировать вызовы malloc с записью стека
perf record -e probe_libc:malloc -ag -- sleep 5
perf report --call-graph=graph
Динамические пробы - это инструмент для случаев, когда нужно ответить на очень конкретный вопрос: сколько раз вызывается эта функция? Сколько времени она выполняется? С какими аргументами? Без перезапуска приложения, без перекомпиляции, на работающей production-системе.
Анализ планировщика и промахов кэша на реальных задачах
Два класса проблем производительности встречаются особенно часто и хорошо поддаются анализу через perf. Первый - избыточные переключения контекста и задержки планировщика. Второй - промахи кэша процессора, которые могут замедлить программу в несколько раз даже при отличном IPC.
# Анализ событий планировщика: задержки, миграции, переключения
perf sched record -- sleep 10
perf sched latency
# Статистика переключений контекста для процесса
perf stat -e context-switches,cpu-migrations -p 1234 sleep 10
# Анализ промахов кэша L1 и L2
perf stat -e \
L1-dcache-loads,L1-dcache-load-misses,\
LLC-loads,LLC-load-misses \
./my_program
Если LLC-load-misses (Last Level Cache misses) составляет более 5-10% от LLC-loads, программа регулярно промахивается мимо кэша и ждёт данных из основной памяти. Типичная причина - обход больших структур данных в непоследовательном порядке или ложное разделение кэша (false sharing) в многопоточном коде.
Для анализа false sharing существует специализированная команда:
perf c2c record -- ./my_multithreaded_program
perf c2c report
perf c2c (cache-to-cache) показывает, какие строки кэша чаще всего становятся предметом разногласий между ядрами процессора. Это именно тот инструмент, когда добавление мьютекса неожиданно замедляет программу вместо ускорения.
perf - это окно в работу системы на уровне, который обычно скрыт за абстракциями. Разница между приложением, которое "просто тормозит", и приложением с понятным профилем производительности - это разница между угадыванием и знанием. Инструмент без перекомпиляции ядра, без daemon-процессов, способный ответить на вопросы от "где тратится CPU" до "сколько раз вызывается функция в сетевом стеке" - стоит времени на изучение.