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

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

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

Начать курс

Введение: почему профилирование — это не роскошь, а необходимость

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

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

1. Статистическое профилирование с gprof: быстрый старт

gprof — это классический профилировщик GNU, который работает на основе статистической выборки. Он не требует специальных прав и идеально подходит для первого анализа.

Как подготовить код

Для работы gprof нужно скомпилировать программу с флагами -pg и -g:

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

После выполнения программы в текущей папке появится файл gmon.out. Чтобы получить читаемый отчет, выполните:

gprof my_program gmon.out > report.txt

Пример: находим медленный алгоритм

Рассмотрим простую программу, которая сортирует массив двумя способами:

#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>

void bubble_sort(std::vector<int>& arr) {
    for (size_t i = 0; i < arr.size(); ++i)
        for (size_t j = 0; j < arr.size() - i - 1; ++j)
            if (arr[j] > arr[j+1])
                std::swap(arr[j], arr[j+1]);
}

void quick_sort_wrapper(std::vector<int>& arr) {
    std::sort(arr.begin(), arr.end());
}

int main() {
    std::vector<int> data(50000);
    std::generate(data.begin(), data.end(), std::rand);
    
    auto copy1 = data;
    auto copy2 = data;
    
    bubble_sort(copy1);
    quick_sort_wrapper(copy2);
    
    return 0;
}

Запустите профилирование. В отчете gprof вы увидите таблицу вызовов. bubble_sort будет занимать 95-99% времени. Это очевидное узкое место — замена пузырьковой сортировки на std::sort даст мгновенный прирост.

Интерпретация результатов

В отчете ищите колонку % time — она показывает долю времени, проведенную в функции. Функции с высоким процентом — ваши кандидаты на оптимизацию.

2. Аппаратное профилирование с Perf: точность до такта

Perf — это современный инструмент для Linux, использующий аппаратные счетчики CPU. Он дает минимальные накладные расходы и позволяет замерять не только время, но и кеш-промахи, промахи ветвлений и другие метрики.

Установка и базовое использование

sudo apt install linux-tools-common linux-tools-generic  # Ubuntu/Debian
perf stat ./my_program   # Общая статистика
perf record ./my_program # Запись профиля
perf report              # Просмотр отчета

Пример: находим промахи кеша

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

const int N = 4096;
int matrix[N][N];

void process_rows() {
    for (int i = 0; i < N; ++i)
        for (int j = 0; j < N; ++j)
            matrix[i][j] *= 2;
}

void process_cols() {
    for (int j = 0; j < N; ++j)
        for (int i = 0; i < N; ++i)
            matrix[i][j] *= 2;
}

Запустите perf stat для каждой функции. Вы увидите, что process_cols имеет на порядок больше cache-misses. Причина — доступ к памяти по столбцам разрушает локальность данных. Оптимизация: транспонировать матрицу или изменить порядок циклов.

Горячие точки (hotspots)

Команда perf report покажет, какие строки кода потребляют больше всего CPU. Ищите аннотированный код с процентом времени рядом с каждой строкой.

3. Симуляция с Valgrind/Callgrind: профилирование без прав root

Если вам нужно не только время, но и детальный анализ кеша, ветвлений и утечек памяти, используйте Valgrind с инструментом Callgrind. Он симулирует выполнение программы, что дает огромную точность, но замедляет работу в 5-20 раз.

Запуск и визуализация

valgrind --tool=callgrind ./my_program
# Создастся файл callgrind.out.XXXX

Для просмотра используйте kcachegrind (графическая утилита):

sudo apt install kcachegrind
kcachegrind callgrind.out.XXXX

Пример: анализ рекурсивного алгоритма

Рассмотрим вычисление чисел Фибоначчи с рекурсией и мемоизацией:

int fib_rec(int n) {
    if (n <= 1) return n;
    return fib_rec(n-1) + fib_rec(n-2);
}

int fib_memo(int n, std::vector<int>& memo) {
    if (n <= 1) return n;
    if (memo[n] != -1) return memo[n];
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo);
    return memo[n];
}

Запустите Callgrind для обеих версий. В kcachegrind вы увидите, что fib_rec вызывает себя миллионы раз (экспоненциальная сложность), а fib_memo — всего 2N раз. Это наглядный пример, как профилировщик показывает не только время, но и количество вызовов.

По

Блоги

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