Ваш C++ код работает медленно? Пора заглянуть под капот
Вы написали идеальный, на первый взгляд, код на C++. Компиляция прошла без ошибок, программа запускается... но работает как-то вяло. Знакомо? Рука тянется переписать всё на ассемблере или срочно добавить многопоточность. Не торопитесь. Скорее всего, проблема не в архитектуре, а в паре «горячих» функций, которые пожирают 90% процессорного времени.
В мире Linux у нас есть мощнейший арсенал инструментов для профилирования C++ кода. Это не магия, это наука. Мы не гадаем, а измеряем. В этом руководстве я покажу вам четыре рабочих подхода: от быстрого «взгляда сверху» до микро-бенчмаркинга. Вы узнаете, как профилировать C++ на Linux как настоящий профи.
Почему догадки — зло, а профилирование — добро
Перед тем как нырять в код, запомните золотое правило оптимизации: «Не оптимизируй то, что не измерено». Ваша интуиция — плохой советчик. Часто мы тратим часы на оптимизацию функции, которая вызывается раз в минуту, и игнорируем банальный цикл в 10 миллионов итераций.
Профилирование даёт ответы на три главных вопроса:
- Где программа тратит больше всего времени?
- Почему это происходит (неэффективный алгоритм, много операций ввода-вывода, промахи кэша)?
- Что изменилось после ваших правок?
Сегодня мы разберём четыре инструмента, которые покроют 99% ваших задач. Поехали!
Инструмент №1: gprof — классика жанра
gprof — это старый, добрый, встроенный профилировщик GNU. Он работает в два этапа: сбор статистики во время выполнения программы и её анализ. Плюсы: не нужно ставить ничего лишнего, есть в каждом дистрибутиве. Минусы: даёт приблизительные результаты и не работает с многопоточными программами из коробки.
Как пользоваться gprof
Шаг 1. Компилируем программу со специальным флагом -pg:
g++ -pg -O2 my_program.cpp -o my_program
Обратите внимание: мы используем -O2. Профилировать отладочную сборку без оптимизаций — бессмысленно, вы получите искажённую картину.
Шаг 2. Запускаем программу. Она выполнится немного медленнее, так как gprof собирает данные.
./my_program
Шаг 3. В текущей папке появится файл gmon.out. Анализируем его:
gprof my_program gmon.out > analysis.txt
Открываем analysis.txt и видим что-то вроде:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls us/call us/call name
60.00 0.06 0.06 10000 6.00 6.00 slow_function
20.00 0.08 0.02 5000 4.00 4.00 fast_function
10.00 0.09 0.01 main
Вуаля! Мы видим, что slow_function заняла 60% времени. Это наш кандидат на оптимизацию.
Пример кода для профилирования
Давайте напишем простую программу, которую будем профилировать всеми инструментами:
#include <iostream>
#include <cmath>
void slow_function(int n) {
volatile double sum = 0.0; // volatile, чтобы компилятор не оптимизировал в ноль
for (int i = 0; i < n; ++i) {
for (int j = 0; j < 1000; ++j) {
sum += std::sin(i) * std::cos(j);
}
}
std::cout << sum << std::endl;
}
void fast_function(int n) {
volatile int count = 0;
for (int i = 0; i < n; ++i) {
count += i % 2;
}
std::cout << count << std::endl;
}
int main() {
slow_function(10000);
fast_function(50000);
return 0;
}
Скомпилируйте с флагом -pg и запустите gprof. Результат вас удивит: slow_function будет доминировать, хотя fast_function вызывается чаще. Вот она, сила профилирования!
Инструмент №2: perf — мощь ядра Linux
perf — это профилировщик, встроенный прямо в ядро Linux. Он не требует перекомпиляции программы и может анализировать даже системные вызовы. perf — это выбор профессионалов, когда нужно копнуть глубоко.
Установка (если вдруг нет):
sudo apt install linux-tools-common linux-tools-generic
Базовое профилирование CPU
Сначала скомпилируем нашу программу без специальных флагов (только -O2):
g++ -O2 example.cpp -o example
Теперь запускаем perf:
sudo perf record ./example
perf соберёт сэмплы (по умолчанию 4000 раз в секунду). После завершения смотрим отчёт:
sudo perf report
Вы увидите интерактивный интерфейс (можно нажимать стрелки). perf покажет не только ваши функции, но и функции библиотек (например, sin из libm). Это неоценимо, когда вы хотите понять, почему стандартная библиотека медленная.
Совет: используйте флаг --call-graph dwarf, чтобы perf строил графы вызовов:
sudo perf record --call-graph dwarf ./example
Секретный режим: топ самых горячих инструкций
Хотите увидеть, какая строка кода тормозит? Используйте perf annotate:
sudo perf annotate slow_function
perf покажет ассемблерный код вашей функции с процентом времени на каждую инструкцию. Вы увидите, что sin и cos — это настоящие «пожиратели» тактов.
Инструмент №3: Valgrind (Callgrind) — детектив по кэшу
Valgrind — это не один инструмент, а целая лаборатория. Нас интересует Callgrind — симулятор кэша и профилировщик. Он работает медленнее (программа тормозит в 10-20 раз