Как тестировать производительность кода Rust при помощи Iai
Everett Pompeii
Что такое бенчмаркинг?
Бенчмаркинг — это практика тестирования производительности вашего кода, чтобы увидеть, насколько быстро (задержка) или сколько (пропускная способность) работы он может выполнить. Этот часто упускаемый из виду этап в разработке программного обеспечения является ключевым для создания и поддержания быстрого и производительного кода. Бенчмаркинг предоставляет необходимые метрики, чтобы разработчики могли понять, насколько хорошо их код работает под различными рабочими нагрузками и условиями. По тем же причинам, по которым вы пишете модульные и интеграционные тесты, чтобы предотвратить регрессию функций, вам следует писать тесты производительности, чтобы предотвратить регрессию производительности. Ошибки производительности — это ошибки!
Напишите FizzBuzz на Rust
Чтобы написать тесты производительности, нам нужен исходный код для оценки. Для начала мы напишем очень простую программу, FizzBuzz.
Правила для FizzBuzz таковы:
Напишите программу, которая выводит целые числа от
1
до100
(включительно):
- Для кратных трём, выводите
Fizz
- Для кратных пяти, выводите
Buzz
- Для кратных и трём и пяти, выводите
FizzBuzz
- Во всех других случаях, выводите число
Есть множество способов написать FizzBuzz. Так что мы выберем мой любимый:
- Создайте функцию
main
- Переберите числа от
1
до100
включительно. - Для каждого числа вычислите модуль (остаток после деления) для
3
и5
. - Используйте образцовое сопоставление для двух остатков.
Если остаток равен
0
, значит число кратно данному фактору. - Если остаток равен
0
для обоих3
и5
, то напечатайтеFizzBuzz
. - Если остаток равен
0
только для3
, то напечатайтеFizz
. - Если остаток равен
0
только для5
, то напечатайтеBuzz
. - В противном случае просто напечатайте число.
Следуйте Шаг за Шагом
Чтобы следовать этому пошаговому руководству, вам потребуется установить Rust.
🐰 Исходный код для этого поста доступен на GitHub
После установки Rust вы можете открыть окно терминала и ввести: cargo init game
Затем перейдите во вновь созданную директорию game
.
Вы должны увидеть директорию под названием src
с файлом main.rs
:
Замените его содержимое приведенной выше реализацией FizzBuzz. Затем запустите cargo run
.
Вывод должен выглядеть так:
🐰 Бах! Вы успешно проходите собеседование по кодированию!
Должен был сгенерироваться новый файл Cargo.lock
:
Прежде чем продолжить, важно обсудить различия между микро- и макро-бенчмаркингом.
Микробенчмаркинг vs Макробенчмаркинг
Существует две основные категории бенчмарков программного обеспечения: микробенчмарки и макробенчмарки.
Микробенчмарки работают на уровне, аналогичном модульным тестам.
Например, бенчмарк для функции, определяющей Fizz
, Buzz
или FizzBuzz
для одного числа, будет микробенчмарком.
Макробенчмарки работают на уровне, аналогичном интеграционным тестам.
Например, бенчмарк для функции, которая запускает полную игру FizzBuzz, от 1
до 100
, будет макробенчмарком.
Вообще, лучше всего тестировать на наименьшем возможном уровне абстракции. В случае бенчмарков это делает их более простыми в поддержке, и помогает уменьшить количество помех в измерениях. Однако, так же как некоторые end-to-end тесты могут быть очень полезными для проверки правильной работы всей системы, макробенчмарки могут быть очень полезными для проверки производительности критически важных мест в вашем программном обеспечении.
Бенчмаркинг в Rust
Три популярных варианта для бенчмаркинга в Rust: libtest bench, Criterion, и Iai.
libtest — это встроенная в Rust система для модульного тестирования и бенчмаркинга.
Хотя libtest bench является частью стандартной библиотеки Rust, она все еще считается нестабильной,
поэтому доступна только в выпусках компилятора nightly
.
Чтобы работать с стабильным компилятором Rust,
необходимо использовать отдельную систему для бенчмаркинга.
Однако ни одна из них не находится в активной разработке.
Самой популярной системой для бенчмаркинга в экосистеме Rust является Criterion.
Она работает как на стабильных, так и на nightly
выпусках компилятора Rust,
и стала фактическим стандартом в сообществе Rust.
Criterion также намного более функциональна по сравнению с libtest bench.
Экспериментальной альтернативой Criterion является Iai от того же создателя. Однако она использует подсчет инструкций вместо времени с помощью системных часов: инструкции ЦП, L1-доступы, L2-доступы и доступ к оперативной памяти. Это позволяет проводить одноразовые измерения, так как эти метрики должны оставаться почти идентичными между запусками.
Все три поддерживаются по Bencher. Зачем выбрать Iai? Iai использует подсчёт инструкций вместо времени стенного часа. Это делает его идеальным для непрерывного бенчмаркинга, то есть для бенчмаркинга в CI. Я бы предложил использовать Iai для непрерывного бенчмаркинга, особенно если вы используете общие исполнители. Важно понимать, что Iai измеряет лишь прокси для того, что вам действительно важно. Разве увеличение количества инструкций с 1000 до 2000 удваивает задержку вашего приложения? Может быть, может быть и нет. По этой причине полезно также запускать параллельные тесты на основе времени стенного часа вместе с тестами по подсчету инструкций.
🐰 Iai не обновлялся больше 3 лет. Поэтому вы можете расматривать использование Iai-Callgrind.
Установка Valgrind
Iai использует инструмент под названием Valgrind для сбора подсчета инструкций. Valgrind поддерживает Linux, Solaris, FreeBSD и MacOS. Однако поддержка MacOS ограничена процессорами x86_64, так как процессоры arm64 (M1, M2 и т.д.) еще не поддерживаются.
На Debian выполните: sudo apt-get install valgrind
На MacOS (только с процессорами x86_64/Intel): brew install valgrind
Рефакторинг FizzBuzz
Для тестирования нашего приложения FizzBuzz нам нужно отделить нашу логику от функции main
программы.
Инструменты для тестирования производительности не могут тестировать функцию main
. Чтобы сделать это, нам нужно внести несколько изменений.
В директории src
создайте новый файл под названием lib.rs
:
Добавьте следующий код в lib.rs
:
play_game
: Принимает беззнаковое целое числоn
, вызываетfizz_buzz
с этим числом и, еслиprint
равноtrue
, выводит результат.fizz_buzz
: Принимает беззнаковое целое числоn
и выполняет собственно логикуFizz
,Buzz
,FizzBuzz
, или число, возвращая результат в виде строки.
Затем обновите main.rs
так, чтобы он выглядел следующим образом:
game::play_game
: Импортируйтеplay_game
из только что созданного нами ящикаgame
сlib.rs
.main
: Основная точка входа в нашу программу, которая проходит через числа1
до100
включительно и вызываетplay_game
для каждого числа, при этомprint
установлен вtrue
.
Бенчмаркинг FizzBuzz
Чтобы протестировать наш код, нам нужно создать директорию benches
и добавить файл для наших бенчмарков, play_game.rs
:
В файл play_game.rs
добавьте следующий код:
- Импортируйте функцию
play_game
из нашего ящикаgame
. - Создайте функцию под названием
bench_play_game
. - Запустите наш макро-бенчмарк в “чёрном ящике”, чтобы компилятор не оптимизировал наш код.
- Итерируйте от
1
до100
включительно. - Для каждого числа вызывайте
play_game
, установивprint
вfalse
.
Теперь нам нужно настроить ящик game
для выполнения наших бенчмарков.
Добавьте следующее в нижнюю часть вашего файла Cargo.toml
:
iai
: Добавьтеiai
как зависимость разработки, так как мы используем его только для тестирования производительности.bench
: Зарегистрируйтеplay_game
как бенчмарк и установитеharness
вfalse
, так как мы будем использовать Iai в качестве нашего тестового приспособления.
Теперь мы готовы протестировать наш код, запустите cargo bench
:
🐰 Это наша первая метрика тестирования производительности!
Наконец, мы можем отдохнуть… Шутка, наши пользователи хотят новый функционал!
Написать FizzBuzzFibonacci на Rust
Наши ключевые показатели эффективности (KPI) снизились, поэтому наш менеджер по продуктам (PM) хочет, чтобы мы добавили новую функцию. После многочисленных брейнстормингов и интервью с пользователями было решено, что просто FizzBuzz недостаточно. Детям сегодняшнего дня хочется новую игру, FizzBuzzFibonacci.
Правила для FizzBuzzFibonacci следующие:
Напишите программу, которая выводит целые числа от
1
до100
(включительно):
- Для кратных трем, вывод
Fizz
- Для кратных пяти, вывод
Buzz
- Для кратных и трем, и пяти, вывод
FizzBuzz
- Для чисел, которые являются частью последовательности Фибоначчи, вывод только
Fibonacci
- Для всех остальных, вывод самого числа
Последовательность Фибоначчи - это последовательность чисел, в которой каждое следующее число является суммой двух предыдущих.
Например, начиная с 0
и 1
, следующим числом в последовательности Фибоначчи будет 1
.
За ним следуют: 2
, 3
, 5
, 8
и так далее.
Числа, которые являются частью последовательности Фибоначчи, известны как числа Фибоначчи. Так что нам придется написать функцию, которая определяет числа Фибоначчи.
Есть много способов записать последовательность Фибоначчи и, аналогично, много способов определить число Фибоначчи. Поэтому мы пойдем моим любимым способом:
- Создайте функцию под названием
is_fibonacci_number
, которая принимает беззнаковое целое число и возвращает булево значение. - Повторяйте для всех чисел от
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
:
Обе наши функции main
и bench_play_game
могут остаться точно такими же.
Бенчмаркинг FizzBuzzFibonacci
Теперь мы можем повторить наш бенчмарк:
О, классно! Iai говорит нам, что разница между оценочным количеством циклов наших игр FizzBuzz и FizzBuzzFibonacci составляет +522.6091%
.
Ваши числа будут немного отличаться от моих.
Однако разница между двумя играми скорее всего в пределах 5x
.
Мне это кажется хорошим! Особенно учитывая, что мы добавили такую звучащую функцию, как Фибоначчи в нашу игру.
Детям это обязательно понравится!
Расширяем FizzBuzzFibonacci на Rust
Наша игра стала хитом! Дети действительно любят играть в FizzBuzzFibonacci.
Настолько сильно, что нам донеслись слухи от боссов, что они хотят создать продолжение.
Но мы живем в современном мире, нам нужен ежегодный повторяющийся доход (ARR), а не разовые покупки!
Новое видение нашей игры - это открытая игра, больше не нужно жить в пределах от 1
до 100
(даже если это включительно).
Нет, мы открываем новые горизонты!
Правила для Open World FizzBuzzFibonacci следующие:
Напишите программу, которая принимает на ввод любое положительное целое число и выводит:
- Для кратных трем, выводит
Fizz
- Для кратных пяти, выводит
Buzz
- Для кратных и трем, и пяти, выводит
FizzBuzz
- Для чисел, которые являются частью последовательности Фибоначчи, выводит только
Fibonacci
- Для всех остальных чисел, выводит само число
Чтобы наша игра работала с любым числом, нам нужно будет принять аргумент командной строки.
Обновите функцию main
так, чтобы она выглядела так:
- Собираем все аргументы (
args
), переданные нашей игре из командной строки. - Получаем первый аргумент, переданный нашей игре, и анализируем его как беззнаковое целое число
i
. - Если парсинг сфейлился или аргумент не был передан, по умолчанию считаем, что в нашу игру играют с
15
на входе. - Наконец, играем в нашу игру с новым разобранным беззнаковым целым числом
i
.
Теперь мы можем играть в нашу игру с любым числом!
Используйте cargo run
, затем --
для передачи аргументов нашей игре:
А если мы опустим или передадим недействительное число:
Вау, это было детальное тестирование! CI проходит. Наши шефы в восторге. Давайте пустим это в продакшн! 🚀
Конец
🐰 … конец вашей карьеры, может быть?
Шутка ли, всё в огне! 🔥
Сначала казалось, что все идет нормально. Но в 02:07 утра в субботу мой пейджер прозвучал:
📟 Ваша игра в огне! 🔥
Выпрыгнув из кровати, я пытался понять, что происходит. Я попытался пройтись по логам, но это было сложно, потому что все постоянно вылетало. Наконец, я нашёл проблему. Дети! Им настолько понравилась наша игра, что они играли в нее аж до миллиона! В свете гениального озарения, я добавил два новых бенчмарка:
- Микро-бенчмарк
bench_play_game_100
для игры с числом сто (100
) - Микро-бенчмарк
bench_play_game_1_000_000
для игры с числом миллион (1_000_000
)
Когда я его запустил, я получил это:
Подождите… подождите…
Что! 6,685 оценочных циклов
x 1,000
должно быть 6,685,000 оценочных циклов
, а не 155,109,206 оценочных циклов
🤯
Несмотря на то что я корректно реализовал код функции последовательности Фибоначчи, где-то у меня есть ошибки в производительности.
Исправляем FizzBuzzFibonacci в Rust
Давайте еще раз взглянем на функцию is_fibonacci_number
:
Теперь, когда я думаю о производительности, я понимаю, что у меня есть ненужный, дополнительный цикл.
Мы можем полностью избавиться от цикла for i in 0..=n {}
и
просто сравнить значение current
с данной числом (n
) 🤦
- Обновите вашу функцию
is_fibonacci_number
. - Инициализируйте последовательность Фибоначчи, начав с
0
и1
, какprevious
иcurrent
числах соответственно. - Итерируйте пока
current
число меньше данного числаn
. - Добавьте
previous
иcurrent
число, чтобы получитьnext
число. - Обновите число
previous
на числоcurrent
. - Обновите число
current
на числоnext
. - Как только
current
становится больше или равно данному числуn
, мы выйдем из цикла. - Проверьте, равно ли
current
число данному числуn
, и верните этот результат.
Теперь давайте снова прогоним те бенчмарки и посмотрим, как мы справились:
О, вау! Наш бенчмарк bench_play_game
вновь возращается к значению, которое было у исходного FizzBuzz.
Хочется помнить точное значение этого показателя. Но прошло уже три недели.
Моя история терминала не хранит столько данных.
А Iai сравнивает только самый последний результат.
Но, думаю, это близко!
Бенчмарк bench_play_game_100
снизился почти в 10 раз, -87.22513%
.
А бенчмарк bench_play_game_1_000_000
упал более чем в 10,000 раз! С 155,109,206 оценочных циклов
до 950
оценочных циклов!
Это -99.99939%
!
🐰 Хорошо, что мы заметили эту ошибку в производительности перед тем как она попала в продакшен… о, погодите…
Отслеживание регрессий производительности в 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 бесплатно.