Библиотека 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();
Как это работает:
- Мы определяем функцию
BM_Addition, которая принимает ссылку наbenchmark::State. - Внутри функции мы используем цикл
for (auto _ : state). Библиотека будет вызывать тело этого цикла многократно, пока не наберет достаточную статистику. benchmark::DoNotOptimize(result)— это ключевая функция. Она говорит компилятору: "Не выбрасывай эту переменную, даже если кажется, что она не нужна". Без неё компилятор может полностью удалить наш код, и мы измерим пустой цикл.- Макрос
BENCHMARK(BM_Addition)регистрирует нашу функцию для запуска. - Макрос
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()
Также в библиотеке
Рекомендаций