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

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

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

Начать курс

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

Оптимизация без измерений — это гадание. Даже опытные разработчики часто ошибаются, предполагая, какой участок кода работает медленнее всего. Профилирование — единственный научный способ ответить на вопрос: «Где именно программа тратит больше всего времени?».

В этой статье мы разберем практические инструменты для профилирования C++ кода на Linux: gprof, perf и Valgrind (Callgrind). Вы узнаете, как запускать профилировщики, интерпретировать их отчеты и находить реальные узкие места.

1. Профилирование с помощью gprof

Подготовка кода

gprof — самый простой и доступный профилировщик. Для его работы нужно скомпилировать программу с флагом -pg:

g++ -pg -O2 main.cpp -o my_program

Запускаем программу. Она выполнится немного медленнее обычного, но создаст файл gmon.out с данными профилирования. Затем запускаем gprof:

./my_program
gprof my_program gmon.out > report.txt

Пример анализа

Рассмотрим простую программу с двумя функциями:

#include <iostream>
#include <cmath>

void slowFunction() {
    double sum = 0.0;
    for (int i = 0; i < 100000000; ++i) {
        sum += std::sin(i) * std::cos(i);
    }
    std::cout << "Sum: " << sum << std::endl;
}

void fastFunction() {
    int count = 0;
    for (int i = 0; i < 1000; ++i) {
        for (int j = 0; j < 1000; ++j) {
            count += (i * j) % 3;
        }
    }
    std::cout << "Count: " << count << std::endl;
}

int main() {
    slowFunction();
    fastFunction();
    return 0;
}

После запуска gprof в отчете мы увидим таблицу с временем выполнения каждой функции:

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 99.0      1.98     1.98        1  1980.0  1980.0  slowFunction()
  1.0      0.02     0.02        1    20.0    20.0  fastFunction()

Вывод очевиден: slowFunction() занимает 99% времени. Узкое место — вызов тригонометрических функций sin и cos в цикле.

Ограничения gprof

  • Работает только с программами, скомпилированными с -pg
  • Не показывает время в inline-функциях
  • Не подходит для многопоточных приложений (без дополнительных настроек)

2. Профилирование с помощью perf (Linux)

Базовый запуск

perf — мощный инструмент для профилирования на уровне ядра. Он не требует перекомпиляции программы и работает с любыми бинарниками (даже с оптимизацией -O3):

# Установка (если нет)
sudo apt install linux-tools-common linux-tools-generic

# Профилирование с подсчетом тактов процессора
perf stat ./my_program

# Запись событий для детального анализа
perf record ./my_program
perf report

Пример с perf stat

Для той же программы perf stat покажет:

Performance counter stats for './my_program':

      1,987.45 msec task-clock                #    1.000 CPUs utilized
            10      context-switches          #    5.028 /sec
             0      cpu-migrations            #    0.000 /sec
           105      page-faults               #   52.832 /sec
  5,234,567,890      cycles                    #    2.634 GHz
  9,876,543,210      instructions              #    1.89  insn per cycle
   1,234,567,890      branches                  #  621.234 M/sec
        10,000,000      branch-misses             #    0.81% of all branches

       1.987456789 seconds time elapsed

Ключевые метрики:

  • instructions per cycle (IPC) — показывает эффективность использования конвейера процессора. Низкий IPC (<1) говорит о проблемах с кэшем или зависимостями данных.
  • branch-misses — частые ошибки предсказания ветвлений замедляют код.

Детальный анализ с perf record/report

perf record собирает выборки (samples) по всем функциям. После запуска perf report мы увидим интерактивный TUI-интерфейс с сортировкой по проценту времени:

Overhead  Command   Shared Object        Symbol
 98.50%   my_prog   my_prog             slowFunction()
  1.50%   my_prog   my_prog             fastFunction()

Совет: Добавьте флаг -g для записи call-graph (стека вызовов):

perf record --call-graph dwarf ./my_program

3. Профилирование с помощью Valgrind (Callgrind)

Запуск и интерпретация

Valgrind — не просто детектор утечек памяти. Инструмент callgrind эмулирует выполнение программы и собирает точную статистику по каждой инструкции. Это медленно, но очень подробно:

valgrind --tool=callgrind ./my_program

После завершения появится файл callgrind.out.12345. Для визуализации используйте kcachegrind (графическая утилита):

sudo apt install kcachegrind
kcachegrind callgrind.out.12345

Что показывает Callgrind

Callgrind считает количество выполненных инструкций, а не время процессора. Это дает:

  • Точную оценку — не зависит от загрузки CPU или частоты процессора
  • Счетчики кэша — сколько раз данные были загружены из L1, L2, RAM
  • Детальный

Блоги

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