Как измерить производительность кода на C++ с помощью Google Benchmark
Everett Pompeii
Что такое бенчмаркинг?
Бенчмаркинг — это практика тестирования производительности вашего кода, чтобы увидеть, насколько быстро (задержка) или сколько (пропускная способность) работы он может выполнить. Этот часто упускаемый из виду этап в разработке программного обеспечения является ключевым для создания и поддержания быстрого и производительного кода. Бенчмаркинг предоставляет необходимые метрики, чтобы разработчики могли понять, насколько хорошо их код работает под различными рабочими нагрузками и условиями. По тем же причинам, по которым вы пишете модульные и интеграционные тесты, чтобы предотвратить регрессию функций, вам следует писать тесты производительности, чтобы предотвратить регрессию производительности. Ошибки производительности — это ошибки!
Напишите FizzBuzz на C++
In order to write benchmarks, we need some source code to benchmark. To start off we are going to write a very simple program, FizzBuzz.
The rules for FizzBuzz are as follows:
Write a program that prints the integers from
1
to100
(inclusive):
- For multiples of three, print
Fizz
- For multiples of five, print
Buzz
- For multiples of both three and five, print
FizzBuzz
- For all others, print the number
There are many ways to write FizzBuzz. So we’ll go with the my favorite:
- Выполняйте итерации от
1
до100
, увеличивая значение на каждом шаге. - Для каждого числа вычисляйте модуль (остаток от деления).
- Если остаток равен
0
, значит число кратно заданному множителю:- Если остаток равен
0
для15
, выведитеFizzBuzz
. - Если остаток равен
0
для3
, выведитеFizz
. - Если остаток равен
0
для5
, выведитеBuzz
.
- Если остаток равен
- В противном случае просто выведите число.
Следуйте шаг за шагом
Чтобы следовать этому пошаговому руководству, вам нужно установить git
, установить cmake
и установить GNU Compiler Collection (GCC) g++
.
🐰 Исходный код для этого поста доступен на GitHub.
Создайте файл на C++ с именем game.cpp
,
и установите его содержимое на вышеупомянутую реализацию FizzBuzz.
Используйте g++
для создания исполняемого файла с именем game
, а затем запустите его.
Вывод должен выглядеть так:
🐰 Бум! Вы разгадываете собеседование по программированию!
Прежде чем двигаться дальше, важно обсудить различия между микро-бенчмаркингом и макро-бенчмаркингом.
Микробенчмаркинг vs Макробенчмаркинг
Существует две основные категории бенчмарков программного обеспечения: микробенчмарки и макробенчмарки.
Микробенчмарки работают на уровне, аналогичном модульным тестам.
Например, бенчмарк для функции, определяющей Fizz
, Buzz
или FizzBuzz
для одного числа, будет микробенчмарком.
Макробенчмарки работают на уровне, аналогичном интеграционным тестам.
Например, бенчмарк для функции, которая запускает полную игру FizzBuzz, от 1
до 100
, будет макробенчмарком.
Вообще, лучше всего тестировать на наименьшем возможном уровне абстракции. В случае бенчмарков это делает их более простыми в поддержке, и помогает уменьшить количество помех в измерениях. Однако, так же как некоторые end-to-end тесты могут быть очень полезными для проверки правильной работы всей системы, макробенчмарки могут быть очень полезными для проверки производительности критически важных мест в вашем программном обеспечении.
Бенчмаркинг в C++
Два популярных варианта для бенчмаркинга в C++: Google Benchmark и Catch2.
Google Benchmark — это надежная и универсальная библиотека для бенчмаркинга на C++, позволяющая разработчикам измерять производительность их кода с высокой точностью. Одним из ключевых преимуществ является простота интеграции в существующие проекты, особенно те, которые уже используют GoogleTest. Google Benchmark предоставляет подробные метрики производительности, включая возможность измерять время работы процессора, реальное время и использование памяти. Он поддерживает широкий диапазон сценариев бенчмаркинга — от простых тестов функций до сложных параметризированных тестов.
Catch2 — это современный заголовочный фреймворк для тестирования на C++, который упрощает процесс написания и выполнения тестов. Одним из основных преимуществ является его простота использования: синтаксис как интуитивно понятный, так и выразительный, позволяя разработчикам быстро и ясно писать тесты. Catch2 поддерживает широкий спектр типов тестов, включая модульные тесты, интеграционные тесты, тесты в стиле разработки через поведение (BDD), а также базовые функции микро-бенчмаркинга.
Оба поддерживаются Bencher. Так почему выбрать Google Benchmark? Google Benchmark легко интегрируется с GoogleTest, который является фактическим стандартом для юнит-тестирования в экосистеме C++. Я бы рекомендовал использовать Google Benchmark для измерения задержки вашего кода, особенно если вы уже используете GoogleTest. То есть Google Benchmark отлично подходит для измерения времени работы программы.
Рефакторинг FizzBuzz
Чтобы протестировать наше приложение FizzBuzz, нам нужно отделить нашу логику от функции main
нашей программы. Бенчмарк-харнессы не могут тестировать функцию main
. Чтобы сделать это, нам нужно внести несколько изменений.
Давайте рефакторим логику FizzBuzz в пару функций в новом файле с именем play_game.cpp
:
fizz_buzz
: Принимает целое числоn
и выполняет фактическую логикуFizz
,Buzz
,FizzBuzz
или числа, возвращая результат в виде строки.play_game
: Принимает целое числоn
, вызываетfizz_buzz
с этим числом и, еслиshould_print
равноtrue
, печатает результат.
Теперь давайте создадим заголовочный файл play_game.h
и добавим в него объявление функции play_game
:
Затем обновим функцию main
в game.cpp
, чтобы использовать определение функции play_game
из заголовочного файла:
Функция main
для нашей программы выполняет итерации по числам от 1
до 100
включительно и вызывает play_game
для каждого числа с should_print
, установленным в true
.
Бенчмаркинг FizzBuzz
Чтобы провести бенчмаркинг нашего кода, нам сначала нужно установить Google Benchmark.
Клонируйте библиотеку:
Перейдите в только что клонированный каталог:
Используйте cmake
, чтобы создать каталог сборки для размещения выходных данных сборки:
Используйте cmake
для генерации файлов системы сборки и загрузки всех зависимостей:
Наконец, соберите библиотеку:
Вернитесь в родительский каталог:
Теперь создадим новый файл с именем benchmark_game.cpp
:
- Импортируйте определения функций из
play_game.h
. - Импортируйте заголовок библиотеки Google
benchmark
. - Создайте функцию с именем
BENCHMARK_game
, которая принимает ссылку наbenchmark::State
. - Итерация по объекту
benchmark::State
. - Для каждой итерации выполните итерацию от
1
до100
включительно.- Вызовите
play_game
с текущим числом иshould_print
, установленным вfalse
.
- Вызовите
- Передайте функцию
BENCHMARK_game
в runnerBENCHMARK
. - Запустите бенчмарк с
BENCHMARK_MAIN
.
Теперь мы готовы произвести бенчмаркинг нашего кода:
🐰 Пора свекле вспахать! У нас есть наши первые метрики бенчмаркинга!
Наконец, мы можем отдохнуть наши усталые головы разработчиков… Шутка, наши пользователи хотят новую функцию!
Написание FizzBuzzFibonacci на C++
Наши ключевые показатели эффективности (KPI) снизились, поэтому наш менеджер по продукту (PM) хочет, чтобы мы добавили новую функцию. После множества мозговых штурмов и интервью с пользователями было решено, что старый добрый FizzBuzz не достаточно. Современные дети хотят новую игру, FizzBuzzFibonacci.
Правила для FizzBuzzFibonacci следующие:
Напишите программу, которая выводит целые числа от
1
до100
(включительно):
- Для кратных трем, вывод
Fizz
- Для кратных пяти, вывод
Buzz
- Для кратных и трем, и пяти, вывод
FizzBuzz
- Для чисел, которые являются частью последовательности Фибоначчи, вывод только
Fibonacci
- Для всех остальных, вывод самого числа
Последовательность Фибоначчи - это последовательность чисел, в которой каждое следующее число является суммой двух предыдущих.
Например, начиная с 0
и 1
, следующим числом в последовательности Фибоначчи будет 1
.
За ним следуют: 2
, 3
, 5
, 8
и так далее.
Числа, которые являются частью последовательности Фибоначчи, известны как числа Фибоначчи. Так что нам придется написать функцию, которая определяет числа Фибоначчи.
Есть много способов записать последовательность Фибоначчи и, аналогично, много способов определить число Фибоначчи. Поэтому мы пойдем моим любимым способом:
- Создайте функцию с именем
is_fibonacci_number
, которая принимает целое число и возвращает значение типаboolean
. - Итерируйтесь по всем числам от
0
до нашего заданного числаn
включительно. - Инициализируйте последовательность Фибоначчи, начиная с
0
и1
в качествеprevious
иcurrent
чисел соответственно. - Продолжайте итерацию, пока
current
число меньше текущей итерацииi
. - Добавьте
previous
иcurrent
числа, чтобы получить следующее числоnext
. - Обновите
previous
число наcurrent
число. - Обновите
current
число на следующее числоnext
. - Как только
current
станет больше или равно заданному числуn
, мы выйдем из цикла. - Проверьте, равно ли
current
число заданному числуn
, и если да, вернитеtrue
. - В противном случае верните
false
.
Теперь нам нужно обновить нашу функцию fizz_buzz
:
- Переименуйте функцию
fizz_buzz
вfizz_buzz_fibonacci
, чтобы сделать ее более описательной. - Вызовите нашу вспомогательную функцию
is_fibonacci_number
. - Если результат из
is_fibonacci_number
равенtrue
, вернитеFibonacci
. - Если результат из
is_fibonacci_number
равенfalse
, выполните ту же самую логикуFizz
,Buzz
,FizzBuzz
или верните число.
Поскольку мы переименовали fizz_buzz
в fizz_buzz_fibonacci
, нам также нужно обновить нашу функцию play_game
:
Как основная функция, так и функция BENCHMARK_game
могут оставаться точно такими же.
Тест производительности FizzBuzzFibonacci
Теперь мы можем снова запустить наш тест производительности:
Прокручивая историю терминала назад, мы можем примерно сравнить производительность наших игр FizzBuzz и FizzBuzzFibonacci: 1698 ns
vs 56190 ns
.
Ваши цифры будут немного отличаться от моих.
Тем не менее, разница между двумя играми, вероятно, будет в диапазоне 50 раз.
Это кажется мне хорошим результатом! Особенно для добавления функции с таким впечатляющим названием, как Фибоначчи, в нашу игру.
Детям это понравится!
Расширение FizzBuzzFibonacci на C++
Наша игра пользуется успехом! Детям действительно нравится играть в FizzBuzzFibonacci.
Настолько, что от руководства поступила просьба выпустить продолжение.
Но это современный мир, нам нужен ежегодный постоянный доход (ARR), а не разовые покупки!
Новая концепция нашей игры — она должна быть безграничной, больше никаких ограничений между 1
и 100
(даже если они включаются).
Нет, мы идем к новым горизонтам!
Правила для Open World FizzBuzzFibonacci следующие:
Напишите программу, которая принимает на ввод любое положительное целое число и выводит:
- Для кратных трем, выводит
Fizz
- Для кратных пяти, выводит
Buzz
- Для кратных и трем, и пяти, выводит
FizzBuzz
- Для чисел, которые являются частью последовательности Фибоначчи, выводит только
Fibonacci
- Для всех остальных чисел, выводит само число
Чтобы наша игра работала для любого числа, нам нужно принять аргумент командной строки.
Обновите функцию main
, чтобы она выглядела следующим образом:
- Обновите функцию
main
для принятияargc
иargv
. - Получите первый аргумент, переданный нашей игре, и проверьте, является ли он цифрой.
- Если да, преобразуйте первый аргумент в целое число,
i
. - Сыграйте в нашу игру с вновь преобразованным целым числом
i
.
- Если да, преобразуйте первый аргумент в целое число,
- Если преобразование не удалось или аргумент не передан, по умолчанию запрашивайте правильный ввод.
Теперь мы можем играть в нашу игру с любым числом!
Перекомпилируйте наш исполняемый файл game
, а затем
запустите исполняемый файл с последующим указанием целого числа, чтобы сыграть в нашу игру:
А если мы пропустим или введем недопустимое число:
Вау, это было тщательное тестирование! CI проходит. Наши боссы в восторге. Давайте выпустим это! 🚀
Конец
🐰 … конец вашей карьеры, может быть?
Шутка ли, всё в огне! 🔥
Сначала казалось, что все идет нормально. Но в 02:07 утра в субботу мой пейджер прозвучал:
📟 Ваша игра в огне! 🔥
Выпрыгнув из кровати, я пытался понять, что происходит. Я попытался пройтись по логам, но это было сложно, потому что все постоянно вылетало. Наконец, я нашёл проблему. Дети! Им настолько понравилась наша игра, что они играли в нее аж до миллиона! В свете гениального озарения, я добавил два новых бенчмарка:
- Микротест
BENCHMARK_game_100
для игры с числом сто (100
) - Микротест
BENCHMARK_game_1_000_000
для игры с числом миллион (1_000_000
)
Когда я его запустил, я получил следующее:
Ждём… ждём…
Что! 1,249 нс
x 10,000
должно быть 12,490,000 нс
, а не 110,879,642 нс
🤯
Хотя моя функция для последовательности Фибоначчи работает корректно, у меня должна быть ошибка в производительности.
Исправление FizzBuzzFibonacci на C++
Давайте еще раз взглянем на функцию is_fibonacci_number
:
Теперь, когда я думаю о производительности, я действительно осознаю, что у меня есть ненужный, лишний цикл.
Мы можем полностью избавиться от цикла for (int i = 0; i <= n; ++i)
и
просто сравнивать значение current
с заданным числом (n
) 🤦
- Обновите нашу функцию
is_fibonacci_number
. - Инициализируйте последовательность Фибоначчи, начиная с
0
и1
в качестве предыдущего и текущего чисел соответственно. - Итерация продолжается, пока
current
меньше заданного числаn
. - Сложите
previous
иcurrent
, чтобы получить следующее число. - Обновите предыдущее число на текущее число.
- Обновите текущее число на следующее число.
- Как только
current
станет больше или равен заданному числуn
, мы выйдем из цикла. - Проверьте, равно ли текущее число заданному числу
n
, и верните этот результат.
Теперь давайте повторно запустим эти тесты и посмотрим, как мы справимся:
О, вау! Наш тест BENCHMARK_game
снова вернулся к тому, где он был для оригинального FizzBuzz.
Жаль, что я не помню точно, какой там был результат. Прошло три недели.
Мой терминал не хранит такие старые результаты, и Google Benchmark их не сохраняет.
Но я думаю, результат близок!
Тест BENCHMARK_game_100
уменьшился почти в 50 раз до 34.4 нс
.
А тест BENCHMARK_game_1_000_000
уменьшился более чем в 1,500,000 раз! С 110,879,642 нс
до 61.6 нс
!
🐰 Как минимум, мы нашли эту ошибку производительности до того, как она попала в продакшн… хотя, неважно…
Отслеживание регрессий производительности в CI
Руководители были недовольны потоком отрицательных отзывов, которые наша игра получила из-за моей ошибки в производительности. Они сказали мне, чтобы это больше не происходило, и когда я спросил как, они просто сказали мне больше этого не делать. Как мне это контролировать‽
К счастью, я нашел этот замечательный инструмент с открытым исходным кодом под названием Bencher. У него есть очень щедрый бесплатный уровень, поэтому я могу использовать Bencher Cloud для своих личных проектов. А на работе, где все должно быть в нашем приватном облаке, я начал использовать Самостоятельный хостинг Bencher.
У Bencher есть встроенные адаптеры, поэтому их легко интегрировать в CI. После прочтения руководства по быстрому старту, я могу запускать свои бенчмарки и отслеживать их с помощью Bencher.
Используя это замечательное устройство для путешествий во времени, которое мне дал милый кролик, Я смог вернуться в прошлое и повторить то, что бы произошло, если бы мы использовали Bencher с самого начала. Вы можете увидеть, где мы впервые внесли ошибочную реализацию FizzBuzzFibonacci. Я немедленно получил ошибки в CI в виде комментария к моему запросу на вытягивание. В тот же день я исправил ошибку производительности, устранив не нужный, лишний цикл. Никаких пожаров. Только довольные пользователи.
Bencher: Непрерывное тестирование производительности
Bencher - это набор инструментов для непрерывного тестирования производительности. Когда-нибудь регрессия производительности влияла на ваших пользователей? Bencher мог бы предотвратить это. Bencher позволяет вам обнаруживать и предотвращать регрессии производительности до того, как они попадут в продакшн.
- Запустить: Запустите свои тесты производительности локально или в CI, используя ваши любимые инструменты для этого. CLI
bencher
просто оборачивает ваш существующий аппарат тестирования и сохраняет его результаты. - Отслеживать: Отслеживайте результаты ваших тестов производительности со временем. Мониторите, запрашивайте и строите графики результатов с помощью веб-консоли Bencher на основе ветки исходного кода, испытательного стенда и меры.
- Поймать: Отлавливайте регрессии производительности в CI. Bencher использует инструменты аналитики, работающие по последнему слову техники, чтобы обнаружить регрессии производительности, прежде чем они попадут в продакшн.
По тем же причинам, по которым модульные тесты запускаются в CI, чтобы предотвратить регрессии функций, тесты производительности должны быть запущены в CI с Bencher, чтобы предотвратить регрессии производительности. Ошибки производительности – это тоже ошибки!
Начните отлавливать регрессии производительности в CI — попробуйте Bencher Cloud бесплатно.