Когда приложение начинает тормозить без видимых причин, первый инстинкт разработчика - добавить логирование, расставить счётчики, поднять 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" до "сколько раз вызывается функция в сетевом стеке" - стоит времени на изучение.