Тестирование

Бенчмаркинг производительности кода с QBENCHMARK и Google Benchmark

Введение

Помимо корректности, для многих частей кода важна производительность — особенно горячих путей выполнения (циклы обработки данных, парсеры, алгоритмы сортировки). Замерять производительность «на глаз» через однократный запуск с секундомером ненадёжно из-за шума (фоновые процессы, прогрев кеша процессора, JIT-подобные эффекты). В статье рассмотрим два инструмента для систематического бенчмаркинга C++/Qt-кода: встроенный QBENCHMARK в QTest и более развитую сторонюю библиотеку Google Benchmark.

Концепция

QBENCHMARK — макрос внутри обычного QTest-теста, который оборачивает измеряемый код и автоматически повторяет его выполнение нужное количество раз для получения статистически устойчивого результата, поддерживая разные backend-измерители (время на стенке, число тактов процессора, через perf на Linux). Это удобно, когда бенчмарки нужно держать рядом с обычными тестами в той же инфраструктуре QTest/ctest.

Google Benchmark — специализированная библиотека с более тонким контролем: автоматическим подбором количества итераций для статистической значимости, поддержкой параметризованных бенчмарков (запуск с разными размерами входных данных для построения кривой сложности), и встроенным сравнением результатов между запусками для отслеживания регрессий производительности.

Пример кода

// --- QBENCHMARK в QTest ---
#include <QtTest/QtTest>
#include <QVector>
#include <algorithm>

class BenchmarkSort : public QObject
{
    Q_OBJECT
private slots:
    void benchmarkQVectorSort()
    {
        QVector<int> data(10000);
        for (int i = 0; i < data.size(); ++i) {
            data[i] = data.size() - i;
        }

        QBENCHMARK {
            QVector<int> copy = data; // копия, чтобы не сортировать уже отсортированное
            std::sort(copy.begin(), copy.end());
        }
    }
};

QTEST_APPLESS_MAIN(BenchmarkSort)
#include "benchmark_sort.moc"
// --- Google Benchmark ---
#include <benchmark/benchmark.h>
#include <vector>
#include <algorithm>

static void BM_VectorSort(benchmark::State &state)
{
    const int size = state.range(0);
    std::vector<int> data(size);
    for (int i = 0; i < size; ++i) {
        data[i] = size - i;
    }

    for (auto _ : state) { // цикл, измеряемый библиотекой
        std::vector<int> copy = data;
        std::sort(copy.begin(), copy.end());
    }

    state.SetComplexityN(size);
}

// Параметризация: запуск с разными размерами входных данных
BENCHMARK(BM_VectorSort)->RangeMultiplier(4)->Range(1000, 1000000)->Complexity();

BENCHMARK_MAIN();

Пояснения к коду

QBENCHMARK в QTest-варианте оборачивает блок кода (создание копии и сортировку), который автоматически выполняется QTest многократно с измерением затраченного времени — результат выводится в стандартный вывод теста при запуске с флагом -vb (verbose benchmark) и фиксируется как часть обычного тестового прогона.

Google Benchmark-вариант более явный: цикл for (auto _ : state) запускается библиотекой нужное число раз для достижения статистической значимости, а BENCHMARK(...)->RangeMultiplier(4)->Range(1000, 1000000) запускает один и тот же бенчмарк с разными размерами входных данных (1000, 4000, 16000 и так далее), что вместе с ->Complexity() позволяет библиотеке автоматически оценить асимптотическую сложность алгоритма (например, подтвердить O(n log n) для сортировки) на основе эмпирических измерений.

Подводные камни

  • Измерение бенчмарка на машине с фоновой нагрузкой (другие процессы, тротлинг процессора из-за нагрева) даёт шумные, плохо воспроизводимые результаты — для надёжных измерений желательно изолированное окружение, отключение турбобуста для стабильности частоты, и усреднение по множеству запусков.
  • Оптимизация компилятором «выбрасывает» измеряемый код, если результат вычислений никуда не используется (dead code elimination) — это особенно опасно для микробенчмарков; Google Benchmark предоставляет benchmark::DoNotOptimize() для предотвращения такой оптимизации, а в QTest нужно следить, чтобы результат хоть как-то «наблюдался» после блока QBENCHMARK.
  • Сравнение бенчмарков, собранных в разных конфигурациях (Debug vs Release). Бенчмарки в Debug-сборке без оптимизаций показывают абсолютные числа, не имеющие отношения к реальной производительности в продакшене — бенчмарки нужно всегда запускать в оптимизированной (Release или RelWithDebInfo) сборке.
  • Игнорирование «холодного» первого прогона (прогрев кеша, ленивая инициализация статических данных), который может искажать усреднённый результат при малом числе итераций — оба инструмента в той или иной степени решают это автоматическим увеличением числа повторов, но для очень коротких операций стоит явно проверять, что число итераций действительно достаточно велико для стабильного результата.