Введение: зачем профилировать 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
- Детальный