Профилирование C++: perf, VTune и flame graph на практике
Сервис на C++ упёрся в CPU, а gprof показывает не то. Разбираем, как найти реальное горячее место через perf, VTune и flame graph — без гадания по коду.

Расчётный сервис на C++ держал 4000 запросов в секунду, после релиза просел до 1600 при той же нагрузке и железе (16 vCPU, Linux 6.x). Профиль CPU показал 70% времени в одной функции сериализации, которую до релиза почти не было видно. Без инструментов это искали бы днями по коду — с perf и flame graph нашли за двадцать минут. Ниже — механика трёх инструментов профилирования, что каждый видит, где врёт, и как читать их вывод, когда цифры не сходятся.
Что вообще меряем
Профилирование C++ распадается на два вопроса, которые постоянно путают. Первый: где программа проводит время CPU (on-CPU profiling). Второй: почему поток не выполняется — ждёт лок, диск, сеть (off-CPU profiling). Инструмент, идеальный для первого, слеп ко второму.
perf и VTune в базовом режиме работают через сэмплирование: с заданной частотой ядро прерывает выполнение и записывает текущий стек. Это дёшево (оверхед единицы процентов) и не требует правки кода. Минус — редкие, но дорогие вызовы могут не попасть в выборку, а стек без символов бесполезен.
Сэмплирующий профайлер отвечает на вопрос «на что уходит время», а не «сколько раз вызвана функция». Если вам нужно точное число вызовов — это другой инструмент (инструментация или счётчики), и оверхед там на порядок выше.
Что нужно до начала
- Сборка с
-g— без отладочных символов профиль покажет адреса вместо имён функций.-O2при этом оставляйте: профилировать нужно то, что поедет в прод, а не debug-сборку. - Для читаемых стеков —
-fno-omit-frame-pointerлибо dwarf-развёртка (о ней ниже). Иначе стек оборвётся на второй-третьей рамке. - Доступ к
perf: значениеkernel.perf_event_paranoidобычно должно быть 1 или ниже для профилирования из-под обычного пользователя. Проверьтеsysctl kernel.perf_event_paranoid; менять — под root и с пониманием, что снижаете изоляцию.
perf: базовый снимок за минуту
Начинают с perf top — живой список горячих функций, аналог top по коду. Он показывает, куда смотреть, но не даёт стеков.
Для полноценного профиля пишут запись и потом отчёт. Ключевой момент — как собирать стеки. Три способа развёртки:
| Режим | Флаг perf record | Требует | Особенность |
|---|---|---|---|
| Frame pointer | --call-graph fp | сборка с frame pointer | дёшево, ломается на оптимизированном коде без FP |
| DWARF | --call-graph dwarf | -g, много места под дамп стека | работает без FP, но тяжелее и ограничен глубиной стека |
| LBR | --call-graph lbr | процессор Intel с Last Branch Record | дёшево и точно, но короткая глубина стека |
Рабочая последовательность для процесса по PID:
# perf 6.x, профилируем работающий процесс 30 секунд
# -F 999 — частота сэмплирования 999 Гц (нечётное, чтобы не совпасть с периодикой)
# -g включает сбор стеков выбранным ранее методом
perf record -F 999 -p $(pgrep -n mysrv) --call-graph dwarf -- sleep 30
# текстовый отчёт, свёрнутый по функциям
perf report --stdio
# только топ по самостоятельному времени (self), без вложенных
perf report --stdio --no-children | head -40
Проверка, что получилось: в perf report вы видите имена ваших функций, а не адреса вроде 0x00007f.... Если видите адреса — символов нет, вернитесь к -g и не забудьте про библиотеки (их debuginfo ставится отдельно).
Разница между колонками Children и Self — то, на чём спотыкаются. Self — время в самой функции, Children — плюс всё, что она вызвала. Функция с большим Children и крошечным Self сама по себе не проблема — проблема ниже по стеку.
Flame graph: почему картинка читается быстрее таблицы
Текстовый perf report с глубокими стеками превращается в простыню. Flame graph (инструмент Брендана Грегга) сворачивает те же данные в SVG: ширина прямоугольника — доля времени, вертикаль — глубина стека. Широкое плато на любом уровне — вот оно, горячее место.
# Скрипты из репозитория brendangregg/FlameGraph
perf record -F 999 -p $(pgrep -n mysrv) --call-graph dwarf -- sleep 30
perf script > out.perf
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > flame.svg
Открываете flame.svg в браузере — по прямоугольникам можно кликать для зума. Читается так:
- Широкая башня целиком — глубокая цепочка вызовов, где время съедается на самом верху. Смотрите на вершину.
- Широкое основание под многими башнями — общий аллокатор или лок, вызываемый отовсюду. Часто это
malloc/freeилиstd::string. - Ось X — не время. Стеки отсортированы по алфавиту, а не по порядку выполнения. Ширина = доля сэмплов, и только.
В том инциденте с сериализацией flame graph показал плато шириной в две трети экрана на функции копирования буфера — новый код на каждый ответ делал лишнюю реаллокацию вектора вместо reserve. В таблице это тонуло среди сотни строк.
VTune: когда perf показал горячую функцию, но непонятно почему
Intel VTune (Intel oneAPI, версии линейки 202x) отвечает на вопрос уровнем ниже: функция горячая — но она упёрлась в вычисления, в память или в неверное предсказание переходов. perf это тоже умеет через аппаратные счётчики, но VTune подаёт готовыми метриками.
Ключевой анализ — Microarchitecture Exploration (бывший General Exploration). Он раскладывает такты по методологии Top-Down: сколько ушло на реальную полезную работу (Retiring), сколько на ожидание памяти (Memory Bound), сколько на промахи предсказателя (Bad Speculation), сколько на нехватку исполнительных портов (Core Bound).
# VTune из oneAPI, сбор hotspots по PID
vtune -collect hotspots -target-pid $(pgrep -n mysrv) -duration 30
# микроархитектурный анализ — покажет, где узкое место: память или ALU
vtune -collect uarch-exploration -target-pid $(pgrep -n mysrv) -duration 30
Если VTune показывает высокий Memory Bound на вашей горячей функции — оптимизировать алгоритм бессмысленно, проблема в раскладке данных: cache miss, разбросанные по памяти структуры, false sharing между потоками. Если высокий Retiring — код делает много полезной работы, и выигрыш только в уменьшении объёма вычислений.
Правило, которое экономит дни: сначала выясните класс проблемы (CPU-bound, memory-bound, off-CPU), потом лезьте в код. Оптимизировать арифметику в memory-bound цикле — это гнать по прямой с ручником.
Когда что брать
| Ситуация | Инструмент |
|---|---|
| Быстро понять, где горячо, прямо в проде | perf top, затем perf record |
| Показать профиль команде, найти неочевидное плато | perf + flame graph |
| Функция горячая, но неясно, упор в память или в вычисления | VTune uarch-exploration |
| Поток простаивает, CPU-профиль пустой | off-CPU: perf sched или off-CPU flame graph |
| Нет root и Intel-специфики, нужен кроссплатформенный вариант | perf (Linux), для macOS — Instruments |
Где всё это врёт и ломается
- Пропущенные стеки без frame pointer. Оптимизированный C++ по умолчанию не хранит FP. Стек обрывается, и время «прилипает» к случайной рамке. Лечится
--call-graph dwarfили пересборкой с-fno-omit-frame-pointer. - Инлайнинг. Компилятор встроил функцию — в профиле её нет, время учтено на вызывающей стороне. Флаг
-gсохраняет inline-информацию, иperf reportпри поддержке её частично разворачивает, но не всегда. - Skid. Сэмпл записывается не ровно на инструкции события, а с задержкой конвейера. Горячая строка может «съехать» на соседнюю. На Intel лечится PEBS (
perf record -e cycles:pp). - Слишком короткий прогон. 30 секунд под нагрузкой — минимум. На простое профиль покажет idle-циклы и системные потоки, а не вашу логику.
- Off-CPU невидим. Если сервис тормозит на локе или на диске, on-CPU профиль покажет низкую загрузку и ничего полезного. Это признак, что нужен off-CPU анализ, а не что «всё быстро».
Что делать дальше
Соберите базовый perf record под реальной нагрузкой и постройте flame graph — это пять минут и половина ответов. Если горячая функция найдена, но неясна причина, снимите uarch-exploration в VTune и посмотрите на Memory Bound против Retiring. Замеряйте до и после каждого изменения на одном железе и одной нагрузке: без числа «было 1600 rps, стало 3800» оптимизация — это вера, а не инженерия. Off-CPU профилирование и разбор false sharing — отдельные темы, здесь мы держались только on-CPU CPU-профиля.
Таможенный брокер (ВЭД): Коды ТН ВЭД, маркировка «Честный ЗНАК», сертификация.
Спросить за 20 ₽Источники: Linux perf wiki — Tutorial, Brendan Gregg — FlameGraph (GitHub)
Частые вопросы
У меня AMD, а не Intel — VTune и LBR не работают?
perf report показывает адреса вместо имён функций, что не так?
Флейм-граф показывает широкое основание на malloc — это нормально?
А если профиль показывает низкую загрузку CPU, но сервис тормозит?
Можно ли профилировать прод без остановки сервиса?
Разница между колонками Self и Children в perf report сбивает с толку — на что смотреть?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.