Профилирование C++ кода: находим узкие места для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему важно профилировать C++ код?

C++ — один из самых быстрых языков программирования, но даже на нём можно написать медленный код. Часто разработчики тратят часы на оптимизацию не тех участков программы, руководствуясь интуицией. Профилирование — это научный подход к оптимизации: вы измеряете, где программа тратит больше всего времени, и целенаправленно улучшаете эти места.

Для начинающих C++ программистов профилирование может казаться сложным, но на самом деле базовые инструменты осваиваются за один вечер. В этой статье мы разберём три популярных инструмента: gprof, perf и Valgrind, и научимся интерпретировать их результаты.

Что такое профилирование и какие бывают методы?

Профилирование — это сбор данных о работе программы во время её выполнения. Существует два основных подхода:

  • Инструментирующее профилирование — в код программы добавляются специальные "зонды" (например, gprof). Минус: замедляет выполнение программы.
  • Сэмплирующее профилирование — через равные промежутки времени "заглядывает" в программу и записывает, какая функция сейчас выполняется (например, perf). Почти не замедляет программу.

Ещё есть инструменты для поиска утечек памяти (Valgrind) и анализа кэша. Для начала достаточно освоить gprof и perf.

Инструмент 1: gprof — классика профилирования

gprof — встроенный профилировщик для GCC. Он работает в два этапа: сбор данных и их анализ.

Как использовать gprof

Сначала скомпилируйте программу с флагом -pg:

g++ -pg -o my_program my_program.cpp

Запустите программу:

./my_program

После выполнения появится файл gmon.out. Теперь запустите gprof:

gprof my_program gmon.out > report.txt

Откройте report.txt и посмотрите на таблицу:

Flat profile:
Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 60.00      0.06     0.06    10000     0.01     0.01  slow_function
 30.00      0.09     0.03     5000     0.01     0.02  medium_function
 10.00      0.10     0.01        1    10.00    10.00  main

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

Совет: Для небольших программ gprof отлично подходит. Для больших проектов он может давать неточные данные из-за оптимизаций компилятора.

Инструмент 2: perf — мощный сэмплер для Linux

perf — это профилировщик, встроенный в ядро Linux. Он работает методом сэмплирования и почти не замедляет программу. Идеален для production-систем.

Базовое использование perf

Скомпилируйте программу с отладочными символами (флаг -g):

g++ -g -o my_program my_program.cpp

Запустите профилирование:

perf record ./my_program

После завершения работы программы, посмотрите отчёт:

perf report

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

Samples: 1K of event 'cycles'
Event count (approx.): 250000000
Overhead  Command      Shared Object     Symbol
  45.32%  my_program   my_program        [.] slow_function
  30.15%  my_program   my_program        [.] medium_function
  12.50%  my_program   libc-2.31.so      [.] malloc
   8.03%  my_program   my_program        [.] main

Важно: perf требует прав root для сбора данных о ядре, но для пользовательских программ можно запускать без sudo.

Продвинутые команды perf

  • perf stat ./my_program — показывает количество инструкций, промахов кэша, ветвлений и другую статистику.
  • perf top — показывает "горячие" функции в реальном времени (аналог top, но для кода).
  • perf annotate — показывает ассемблерный код с аннотацией, какие инструкции выполняются чаще всего.

Инструмент 3: Valgrind — профилирование и поиск утечек

Valgrind — это не просто профилировщик, а целый набор инструментов. Самый популярный — Callgrind (профилирование) и Massif (анализ памяти).

Профилирование с Callgrind

Запустите программу под Valgrind:

valgrind --tool=callgrind ./my_program

После выполнения появится файл callgrind.out.XXXX. Чтобы его прочитать, используйте callgrind_annotate:

callgrind_annotate callgrind.out.XXXX

Вывод показывает количество инструкций, выполненных каждой функцией:

--------------------------------------------------------------------------------
Ir                     file:function
--------------------------------------------------------------------------------
1,200,000,000  ???:slow_function [/home/user/my_program]
  800,000,000  ???:medium_function [/home/user/my_program]
  200,000,000  ???:main [/home/user/my_program]

Число Ir — это количество выполненных инструкций. Чем оно больше, тем "тяжелее" функция.

Анализ памяти с Massif

Massif показывает, сколько памяти выделяет программа в каждый момент времени:

valgrind --tool=massif ./my_program
ms_print massif.out.XXXX

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

Блоги

Рекомендаций по книгам