Avatar ·

Профилирование C++ кода на Linux: как найти узкие места и ускорить программу?

Всем привет! Я пишу на C++ довольно давно, но никогда серьёзно не занимался профилированием. Сейчас столкнулся с проблемой: моя программа, которая обрабатывает большие объёмы данных, работает слишком медленно. Она парсит CSV-файлы, выполняет некоторые математические расчёты и записывает результаты в бинарный файл. На тестовом наборе из 500 МБ данных выполнение занимает около 4 минут, а хотелось бы уложиться в 30 секунд. Вот упрощённый пример того, что я делаю: ```cpp #include #include #include #include std::vector processData(const std::vector& input) { std::vector result; result.reserve(input.size()); for (size_t i = 0; i < input.size(); ++i) { double val = input[i]; // Какие-то вычисления for (int j = 0; j < 1000; ++j) { val = std::sin(val * 1.5) + std::cos(val * 0.7); } result.push_back(val); } return result; } int main() { std::ifstream file("data.csv"); std::vector data; double value; while (file >> value) { data.push_back(value); } auto processed = processData(data); std::ofstream out("output.bin", std::ios::binary); out.write(reinterpret_cast(processed.data()), processed.size() * sizeof(double)); return 0; } ``` Я использую Ubuntu 22.04, компилирую с GCC 11 и флагами `-O2 -march=native`. Подскажите, как правильно профилировать такой код? Какие инструменты есть под Linux? Я слышал про gprof, perf и Valgrind, но не понимаю, что выбрать и как их использовать. Хочется увидеть, какие функции занимают больше всего времени, и понять, где именно программа тормозит. <<>>

Лучший способ профилирования C++ на Linux: perf + FlameGraph

Ваша проблема типична: вы написали код, который работает, но неэффективно. Профилирование поможет найти узкие места. Я рекомендую использовать perf — это встроенный в ядро Linux инструмент с минимальными накладными расходами. Он работает на уровне выборки (sampling), не замедляя программу.

Шаг 1: Установка и компиляция с отладочными символами

Установите perf и соберите программу с флагами -g -O2 (флаг -g добавляет отладочные символы для читаемых стеков):

sudo apt install linux-tools-common linux-tools-generic
g++ -g -O2 -march=native -o myapp main.cpp

Шаг 2: Запись профиля

Запустите программу под perf с частотой выборки 99 Гц (стандартная частота, избегающая синхронизации с системным таймером):

sudo perf record -F 99 -g ./myapp
# После завершения появится файл perf.data

Шаг 3: Просмотр отчёта

Интерактивный просмотр:

perf report -g graph

Вы увидите таблицу с функциями, отсортированными по проценту времени. Нажмите Enter на функции, чтобы развернуть стек вызовов. Скорее всего, вы увидите, что std::sin и std::cos занимают 80-90% времени — это ожидаемо из-за вложенного цикла.

Шаг 4: Визуализация с FlameGraph

Для наглядности сгенерируйте флейм-график (огненную диаграмму):

# Скачайте скрипты
git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph
# Преобразуйте perf.data в стектрейсы
sudo perf script > out.perf
# Создайте SVG
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > profile.svg

Откройте profile.svg в браузере. Ширина каждого прямоугольника пропорциональна времени, проведённому в функции. Кликнув на прямоугольник, вы увидите его долю и контекст вызова.

Что делать с результатами?

В вашем случае очевидное узкое место — вложенный цикл из 1000 итераций с вызовом тригонометрических функций. Решения:

  • Оптимизация алгоритма: пересмотрите необходимость 1000 итераций. Возможно, достаточно 10-20.
  • Таблицы предвычислений (lookup tables): если аргументы повторяются, вычислите sin/cos один раз.
  • Использование SIMD: библиотеки вроде Intel MKL или Agner Fog's vector class могут ускорить вычисления.
  • Упрощение: замените sin(val*1.5)+cos(val*0.7) на аппроксимацию полиномами.

Альтернативы perf

Если вам нужен простой старт, попробуйте Valgrind --tool=callgrind. Он эмулирует каждую инструкцию, давая точные данные, но замедляет программу в 10-50 раз. Для 4-минутного выполнения это неприемлемо — perf здесь лучший выбор.

Также можно использовать gprof, но он требует специальной компиляции (-pg) и даёт менее точные результаты из-за инструментирования вызовов функций. Я рекомендую начать с perf — это промышленный стандарт.

<<>>

Профилирование с помощью Google Benchmark и микро-бенчмарки

Если вы хотите не только найти узкие места, но и проверить гипотезы об ускорении, используйте библиотеку Google Benchmark. Она позволяет изолировать отдельные функции и замерять их производительность с высокой точностью.

Пример для вашего кода:

#include <benchmark/benchmark.h>
#include <vector>
#include <cmath>

static void BM_ProcessData(benchmark::State& state) {
    std::vector<double> input(10000);
    for (auto& v : input) v = 1.5;
    for (auto _ : state) {
        auto result = processData(input); // ваша функция
        benchmark::DoNotOptimize(result);
    }
}
BENCHMARK(BM_ProcessData);
BENCHMARK_MAIN();

Скомпилируйте с -lbenchmark -lpthread и запустите. Вы получите время выполнения в микросекундах, количество итераций и статистику. Затем можно модифицировать функцию (например, уменьшить количество итераций цикла) и сравнить результаты.

Google Benchmark полезен для A/B-тестирования оптимизаций. Он автоматически подбирает количество итераций для стабильных замеров и сообщает о выбросах. Это дополнительный инструмент к perf: сначала perf показывает общую картину, потом Google Benchmark детально проверяет конкретные функции.

Установка: sudo apt install libbenchmark-dev или соберите из исходников с GitHub.