benchmark для C++: полное руководство с примерами

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

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

Начать курс

Библиотека benchmark (Google Benchmark) для C++: полное руководство

Добро пожаловать на PythonLib! Сегодня мы погрузимся в мир высокопроизводительного C++ и разберем библиотеку benchmark (также известную как Google Benchmark). Это незаменимый инструмент для каждого разработчика, который хочет писать быстрый и эффективный код. В этой статье мы рассмотрим всё: от установки до продвинутых методов и реальных примеров.

1. Что это и зачем нужна?

Google Benchmark — это библиотека для микробенчмаркинга (microbenchmarking) на языке C++. Она предоставляет мощный и гибкий фреймворк для измерения производительности небольших фрагментов кода (функций, алгоритмов, контейнеров). В отличие от простого замера времени с помощью std::chrono, benchmark автоматически решает множество проблем: разогрев кэша, устранение оптимизаций компилятора, статистическую обработку результатов и многое другое.

Зачем это нужно? В современной разработке производительность часто критична. Без точных измерений вы рискуете:

  • Оптимизировать не то: Тратить время на улучшение кода, который и так работает быстро, в то время как узким местом является другой участок.
  • Ухудшить производительность: Вносить изменения, которые кажутся логичными, но на деле замедляют код из-за особенностей архитектуры процессора или компилятора.
  • Принять неверное архитектурное решение: Выбрать алгоритм или структуру данных, основываясь на интуиции, а не на фактах.

Библиотека benchmark решает эти проблемы, предоставляя надежный, воспроизводимый и статистически обоснованный способ измерения производительности. Она используется в таких гигантах, как Google, LLVM, Facebook и многих других проектах с открытым исходным кодом.

2. Установка

Установка библиотеки benchmark может быть выполнена несколькими способами. Мы рассмотрим два самых популярных: через пакетный менеджер vcpkg и сборку из исходников с помощью CMake.

Способ 1: Установка через vcpkg (рекомендуется)

vcpkg — это кроссплатформенный менеджер пакетов для C++. Если он у вас еще не установлен, следуйте инструкциям на официальном сайте.

После установки vcpkg выполните команду:

vcpkg install benchmark

Для интеграции с вашим проектом на CMake добавьте в CMakeLists.txt:

find_package(benchmark REQUIRED)
target_link_libraries(your_target benchmark::benchmark)

Способ 2: Сборка из исходников

Если вы предпочитаете собирать всё вручную или у вас нет vcpkg, следуйте этим шагам:

# Клонируем репозиторий
git clone https://github.com/google/benchmark.git
cd benchmark

# Создаем директорию для сборки
mkdir build && cd build

# Конфигурируем и собираем библиотеку
cmake .. -DCMAKE_BUILD_TYPE=Release -DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON
make -j$(nproc)

# Устанавливаем (опционально, можно использовать собранную библиотеку напрямую)
sudo make install

После установки вы сможете линковаться к библиотеке, указав пути к заголовочным файлам и библиотекам в вашем проекте.

3. Быстрый старт — минимальный рабочий пример

Давайте напишем простейший бенчмарк, который измеряет скорость сложения двух чисел.

#include <benchmark/benchmark.h>

// Функция, которую мы хотим замерить
static void BM_Addition(benchmark::State& state) {
    int a = 5;
    int b = 10;
    int result = 0;
    for (auto _ : state) {
        // Код внутри этого цикла будет измеряться
        result = a + b;
        // Важно: компилятор может оптимизировать result, если он не используется.
        // Используем benchmark::DoNotOptimize, чтобы предотвратить это.
        benchmark::DoNotOptimize(result);
    }
}

// Регистрируем бенчмарк
BENCHMARK(BM_Addition);

// Точка входа для бенчмарка (генерируется макросом)
BENCHMARK_MAIN();

Как это работает:

  1. Мы определяем функцию BM_Addition, которая принимает ссылку на benchmark::State.
  2. Внутри функции мы используем цикл for (auto _ : state). Библиотека будет вызывать тело этого цикла многократно, пока не наберет достаточную статистику.
  3. benchmark::DoNotOptimize(result) — это ключевая функция. Она говорит компилятору: "Не выбрасывай эту переменную, даже если кажется, что она не нужна". Без неё компилятор может полностью удалить наш код, и мы измерим пустой цикл.
  4. Макрос BENCHMARK(BM_Addition) регистрирует нашу функцию для запуска.
  5. Макрос BENCHMARK_MAIN() создает функцию main(), которая запускает все зарегистрированные бенчмарки.

Скомпилируйте и запустите:

g++ -std=c++11 -isystem benchmark/include -Lbenchmark/build/src -lpthread -lbenchmark my_benchmark.cpp -o my_benchmark
./my_benchmark

Вы увидите вывод, похожий на этот:

2024-01-15T10:30:00+03:00
Running ./my_benchmark
Run on (8 X 2400 MHz CPU s)
CPU Caches:
  L1 Data 32K (x4)
  L1 Instruction 32K (x4)
  L2 Unified 256K (x4)
  L3 Unified 8192K (x1)
Load Average: 0.12, 0.23, 0.18
-------------------------------------------------------------
Benchmark                   Time             CPU   Iterations
-------------------------------------------------------------
BM_Addition              0.000 ns        0.000 ns   1000000000

Время близко к нулю, потому что сложение двух целых чисел — это одна инструкция процессора. Для более сложных функций время будет ненулевым.

4. Основные методы, функции и классы

Теперь давайте подробно разберем ключевые компоненты библиотеки. Мы рассмотрим минимум 8 элементов.

4.1. Класс benchmark::State

Сигнатура: class benchmark::State

Что делает: Это центральный класс, который управляет выполнением одного бенчмарка. Он предоставляет итератор для цикла, хранит статистику и позволяет настраивать параметры теста.

Пример:

static void BM_Example(benchmark::State& state) {
    for (auto _ : state) {
        // Измеряемый код
    }
}

4.2. Функция benchmark::DoNotOptimize()

Сигнатура: template <class T> void benchmark::DoNotOptimize(T& value) или template <class T> void benchmark::DoNotOptimize(const T& value)

Что делает: Предотвращает оптимизацию компилятором переданной переменной. Компилятор не может удалить или переупорядочить операции, связанные с этой переменной. Это критически важно для получения точных результатов.

Пример:

static void BM_DoNotOptimizeExample(benchmark::State& state) {
    int x = 0;
    for (auto _ : state) {
        x = x + 1;
        // Без DoNotOptimize компилятор может вычислить x только один раз
        benchmark::DoNotOptimize(x);
    }
}

4.3. Функция benchmark::ClobberMemory()

Сигнатура: void benchmark::ClobberMemory()

Что делает: Говорит компилятору, что вся память могла быть изменена. Это более сильное средство, чем DoNotOptimize. Оно заставляет компилятор перечитывать все переменные из памяти, предотвращая переупорядочивание операций с памятью. Полезно для тестирования алгоритмов, которые работают с большими объемами данных.

Пример:

static void BM_ClobberMemoryExample(benchmark::State& state) {
    int data[100];
    for (auto _ : state) {
        for (int i = 0; i < 100; ++i) {
            data[i] = i;
        }
        // Заставляем компилятор считать, что data могла измениться
        benchmark::ClobberMemory();
    }
}

4.4. Метод State::KeepRunning()

Сигнатура: bool State::KeepRunning()

Что делает: Альтернатива циклу for (auto _ : state). Используется в более сложных случаях, когда нужно контролировать количество итераций или выполнять дополнительные действия между ними. Возвращает true, пока бенчмарк должен продолжаться.

Пример:

static void BM_KeepRunningExample(benchmark::State& state) {
    int i = 0;
    while (state.KeepRunning()) {
        i++;
    }
    benchmark::DoNotOptimize(i);
}

4.5. Метод State::SetLabel()

Сигнатура: void State::SetLabel(const char* label)

Что делает: Добавляет текстовую метку к результатам бенчмарка. Это полезно для идентификации различных конфигураций или версий тестируемого кода.

Пример:

static void BM_WithLabel(benchmark::State& state) {
    for (auto _ : state) {
        // ... какой-то код
    }
    state.SetLabel("Version 2.0");
}
BENCHMARK(BM_WithLabel);

4.6. Метод State::SetComplexityN()

Сигнатура: void State::SetComplexityN(int n)

Что делает: Устанавливает размер входных данных n для анализа вычислительной сложности. В сочетании с макросом BENCHMARK_DONT_OPTIMIZE и функцией benchmark::DoNotOptimize позволяет библиотеке автоматически оценить сложность алгоритма (O(n), O(n^2), O(log n) и т.д.).

Пример:

static void BM_Complexity(benchmark::State& state) {
    int n = state.range(0); // Получаем размер из аргумента
    std::vector<int> vec(n);
    for (auto _ : state) {
        for (int i = 0; i < n; ++i) {
            vec[i] = i;
        }
        benchmark::DoNotOptimize(vec.data());
    }
    state.SetComplexityN(n);
}
BENCHMARK(BM_Complexity)->RangeMultiplier(2)->Range(1<<10, 1<<20)->Complexity();

4.7. Макрос BENCHMARK()

Сигнатура: BENCHMARK(function_name)

Что делает: Регистрирует функцию как бенчмарк. Возвращает объект benchmark::internal::Benchmark*, который можно дополнительно настраивать с помощью методов, таких как ->Arg(), ->Range(), ->Threads() и т.д.

Пример:

static void BM_MyTest(benchmark::State& state) { /* ... */ }
// Регистрируем и добавляем аргументы
BENCHMARK(BM_MyTest)->Arg(100)->Arg(1000)->Arg(10000);

4.8. Макрос BENCHMARK_MAIN()

Сигнатура: BENCHMARK_MAIN()

Что делает: Генерирует функцию main(), которая инициализирует библиотеку, запускает все зарегистрированные бенчмарки и выводит результаты. Это самый простой способ запуска.

Пример:

// В конце файла с бенчмарками
BENCHMARK_MAIN();

4.9. Метод Benchmark::Arg()

Сигнатура: Benchmark* Benchmark::Arg(int64_t x)

Что делает: Добавляет один аргумент для бенчмарка. Бенчмарк будет запущен один раз для каждого переданного аргумента. Аргумент можно получить внутри функции через state.range(0).

Пример:

static void BM_ArgExample(benchmark::State& state) {
    int n = state.range(0);
    std::vector<int> v(n);
    for (auto _ : state) {
        for (int i = 0; i < n; ++i) {
            v[i] = i;
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_ArgExample)->Arg(10)->Arg(100)->Arg(1000);

4.10. Метод Benchmark::Range()

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