Введение
Помимо корректности, для многих частей кода важна производительность — особенно горячих путей выполнения (циклы обработки данных, парсеры, алгоритмы сортировки). Замерять производительность «на глаз» через однократный запуск с секундомером ненадёжно из-за шума (фоновые процессы, прогрев кеша процессора, 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) сборке.
- Игнорирование «холодного» первого прогона (прогрев кеша, ленивая инициализация статических данных), который может искажать усреднённый результат при малом числе итераций — оба инструмента в той или иной степени решают это автоматическим увеличением числа повторов, но для очень коротких операций стоит явно проверять, что число итераций действительно достаточно велико для стабильного результата.