Как выполнять бенчмаркинг кода Rust с помощью Gungraun

Everett Pompeii

Everett Pompeii


Что такое бенчмаркинг?

Бенчмаркинг — это практика тестирования производительности вашего кода, чтобы увидеть, насколько быстро (задержка) или сколько (пропускная способность) работы он может выполнить. Этот часто упускаемый из виду этап в разработке программного обеспечения является ключевым для создания и поддержания быстрого и производительного кода. Бенчмаркинг предоставляет необходимые метрики, чтобы разработчики могли понять, насколько хорошо их код работает под различными рабочими нагрузками и условиями. По тем же причинам, по которым вы пишете модульные и интеграционные тесты, чтобы предотвратить регрессию функций, вам следует писать тесты производительности, чтобы предотвратить регрессию производительности. Ошибки производительности — это ошибки!

Напишите FizzBuzz на Rust

Чтобы написать тесты производительности, нам нужен исходный код для оценки. Для начала мы напишем очень простую программу, FizzBuzz.

Правила для FizzBuzz таковы:

Напишите программу, которая выводит целые числа от 1 до 100 (включительно):

  • Для кратных трём, выводите Fizz
  • Для кратных пяти, выводите Buzz
  • Для кратных и трём и пяти, выводите FizzBuzz
  • Во всех других случаях, выводите число

Есть множество способов написать FizzBuzz. Так что мы выберем мой любимый:

fn main() {
for i in 1..=100 {
match (i % 3, i % 5) {
(0, 0) => println!("FizzBuzz"),
(0, _) => println!("Fizz"),
(_, 0) => println!("Buzz"),
(_, _) => println!("{i}"),
}
}
}
  • Создайте функцию main
  • Переберите числа от 1 до 100 включительно.
  • Для каждого числа вычислите модуль (остаток после деления) для 3 и 5.
  • Используйте образцовое сопоставление для двух остатков. Если остаток равен 0, значит число кратно данному фактору.
  • Если остаток равен 0 для обоих 3 и 5, то напечатайте FizzBuzz.
  • Если остаток равен 0 только для 3, то напечатайте Fizz.
  • Если остаток равен 0 только для 5, то напечатайте Buzz.
  • В противном случае просто напечатайте число.

Следуйте Шаг за Шагом

Чтобы следовать этому пошаговому руководству, вам потребуется установить Rust.

🐰 Исходный код для этого поста доступен на GitHub

После установки Rust вы можете открыть окно терминала и ввести: cargo init game

Затем перейдите во вновь созданную директорию game.

game
├── Cargo.toml
└── src
└── main.rs

Вы должны увидеть директорию под названием src с файлом main.rs:

fn main() {
println!("Hello, world!");
}

Замените его содержимое приведенной выше реализацией FizzBuzz. Затем запустите cargo run. Вывод должен выглядеть так:

$ cargo run
Compiling playground v0.0.1 (/home/bencher)
Finished dev [unoptimized + debuginfo] target(s) in 0.44s
Running `target/debug/game`
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
97
98
Fizz
Buzz

🐰 Бах! Вы успешно проходите собеседование по кодированию!

Должен был сгенерироваться новый файл Cargo.lock:

game
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs

Прежде чем продолжить, важно обсудить различия между микро- и макро-бенчмаркингом.

Микробенчмаркинг 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. Так почему стоит выбрать Gungraun (переименованный преемник Iai-Callgrind)? Gungraun использует счётчики инструкций вместо реального времени. Это делает его идеальным для непрерывного бенчмаркинга, то есть бенчмаркинга в CI. Я бы рекомендовал использовать Gungraun для непрерывного бенчмаркинга, особенно если вы используете общие раннеры. Gungraun активно поддерживается и имеет исчерпывающую онлайн-документацию, что делает его надёжным выбором для долгосрочных проектов. Важно понимать, что Gungraun измеряет лишь прокси того, что вас действительно интересует. Удваивается ли задержка вашего приложения при переходе от 1 000 инструкций к 2 000 инструкций? Может быть да, а может быть нет. По этой причине может быть полезно также запускать бенчмарки на основе реального времени параллельно с бенчмарками на основе счётчиков инструкций.

Установка Valgrind

Gungraun использует инструмент 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 нашей программы. В отличие от других фреймворков бенчмаркинга, Gungraun может выполнять бенчмарк бинарного файла бенчмарка и функции main, но это чисто макро-бенчмаркинг. Мы хотим выполнять и макро, и микро. Для этого нам нужно внести несколько изменений.

В директории src создайте новый файл с именем lib.rs:

game
├── Cargo.lock
├── Cargo.toml
└── src
└── lib.rs
└── main.rs

Добавьте следующий код в lib.rs:

pub fn play_game(n: u32, print: bool) {
let result = fizz_buzz(n);
if print {
println!("{result}");
}
}
pub fn fizz_buzz(n: u32) -> String {
match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_string(),
(0, _) => "Fizz".to_string(),
(_, 0) => "Buzz".to_string(),
(_, _) => n.to_string(),
}
}
  • play_game: Принимает беззнаковое целое число n, вызывает fizz_buzz с этим числом, и если print равно true, выводит результат.
  • fizz_buzz: Принимает беззнаковое целое число n и выполняет фактическую логику Fizz, Buzz, FizzBuzz или числа, возвращая результат в виде строки.

Затем обновлённый main.rs выглядит так:

use game::play_game;
fn main() {
for i in 1..=100 {
play_game(i, true);
}
}
  • game::play_game: Импортируйте play_game из крейта game, который мы только что создали с помощью lib.rs.
  • main: Главная точка входа в нашу программу, которая перебирает числа от 1 до 100 включительно и вызывает play_game для каждого числа, с print установленным в true.

Бенчмаркинг FizzBuzz

Чтобы выполнить бенчмарк нашего кода, нам нужно создать директорию benches и добавить файл для наших бенчмарков, play_game.rs. Обратите внимание, что мы отклоняемся от рекомендуемого способа структурирования бенчмарков ради простоты. Для вашего проекта следуйте рекомендациям:

game
├── Cargo.lock
├── Cargo.toml
└── benches
└── play_game.rs
└── src
└── lib.rs
└── main.rs

Внутри play_game.rs добавьте следующий код:

use gungraun::prelude::*;
use std::hint::black_box;
use game::play_game;
#[library_benchmark]
fn bench_play_game() {
for i in 1..=100 {
play_game(black_box(i), black_box(false))
}
}
library_benchmark_group!(
name = bench_play_game_group,
benchmarks = [bench_play_game]
);
main!(library_benchmark_groups = bench_play_game_group);
  • Импортируйте модуль gungraun::prelude, который предоставляет необходимые макросы.
  • Используйте std::hint::black_box, чтобы предотвратить оптимизацию компилятором нашего бенчмарка.
  • Импортируйте функцию play_game из нашего крейта game.
  • Создайте функцию библиотечного бенчмарка с именем bench_play_game, используя атрибут #[library_benchmark].
  • Выполните цикл от 1 до 100 и вызовите play_game с print установленным в false.
  • Создайте группу библиотечных бенчмарков с именем bench_play_game_group, содержащую наш бенчмарк bench_play_game.
  • Используйте макрос main! для запуска группы бенчмарков.

Теперь нам нужно настроить крейт game для запуска наших бенчмарков.

Добавьте следующее в конец вашего файла Cargo.toml:

[dev-dependencies]
gungraun = "0.18.0"
[[bench]]
name = "play_game"
harness = false
[profile.bench]
debug = true
  • gungraun: Добавьте gungraun как зависимость для разработки, так как мы используем его только для тестирования производительности.
  • bench: Зарегистрируйте play_game как бенчмарк и установите harness в false, так как мы будем использовать Gungraun в качестве нашего фреймворка бенчмаркинга.
  • debug = true: Включите отладочную информацию в сборках бенчмарков, что необходимо для предоставления Gungraun подробного вывода.

Теперь мы готовы выполнить бенчмарк нашего кода, запустите cargo bench:

$ cargo bench
Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s
Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)
play_game::bench_play_game_group::bench_play_game_100
Instructions: 17902|N/A (*********)
L1 Hits: 24984|N/A (*********)
LL Hits: 1|N/A (*********)
RAM Hits: 20|N/A (*********)
Total read+write: 25005|N/A (*********)
Estimated Cycles: 25689|N/A (*********)
Gungraun result: Ok. 1 without regressions; 0 regressed; 0 filtered; 1 benchmarks finished in 0.15258s

🐰 Салат репа свёкла! У нас есть наши первые метрики бенчмарка!

Наконец, мы можем дать отдых нашим усталым головам разработчиков… Шутка, наши пользователи хотят новую функцию!

Написать FizzBuzzFibonacci на Rust

Наши ключевые показатели эффективности (KPI) снизились, поэтому наш менеджер по продуктам (PM) хочет, чтобы мы добавили новую функцию. После многочисленных брейнстормингов и интервью с пользователями было решено, что просто FizzBuzz недостаточно. Детям сегодняшнего дня хочется новую игру, FizzBuzzFibonacci.

Правила для FizzBuzzFibonacci следующие:

Напишите программу, которая выводит целые числа от 1 до 100 (включительно):

  • Для кратных трем, вывод Fizz
  • Для кратных пяти, вывод Buzz
  • Для кратных и трем, и пяти, вывод FizzBuzz
  • Для чисел, которые являются частью последовательности Фибоначчи, вывод только Fibonacci
  • Для всех остальных, вывод самого числа

Последовательность Фибоначчи - это последовательность чисел, в которой каждое следующее число является суммой двух предыдущих. Например, начиная с 0 и 1, следующим числом в последовательности Фибоначчи будет 1. За ним следуют: 2, 3, 5, 8 и так далее. Числа, которые являются частью последовательности Фибоначчи, известны как числа Фибоначчи. Так что нам придется написать функцию, которая определяет числа Фибоначчи.

Есть много способов записать последовательность Фибоначчи и, аналогично, много способов определить число Фибоначчи. Поэтому мы пойдем моим любимым способом:

fn is_fibonacci_number(n: u32) -> bool {
for i in 0..=n {
let (mut previous, mut current) = (0, 1);
while current < i {
let next = previous + current;
previous = current;
current = next;
}
if current == n {
return true;
}
}
false
}
  • Создайте функцию под названием 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:

pub fn fizz_buzz_fibonacci(n: u32) -> String {
if is_fibonacci_number(n) {
"Fibonacci".to_string()
} else {
match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_string(),
(0, _) => "Fizz".to_string(),
(_, 0) => "Buzz".to_string(),
(_, _) => n.to_string(),
}
}
}
  • Переименуйте функцию 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:

pub fn play_game(n: u32, print: bool) {
let result = fizz_buzz_fibonacci(n);
if print {
println!("{result}");
}
}

Обе наши функции main и bench_play_game могут остаться точно такими же.

Бенчмаркинг FizzBuzzFibonacci

Теперь мы можем повторно запустить наш бенчмарк:

$ cargo bench
Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s
Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)
play_game::bench_play_game_group::bench_play_game_100
Instructions: 331835|17902 (+1753.62%) [+18.5362x]
L1 Hits: 338828|24984 (+1256.18%) [+13.5618x]
LL Hits: 2|1 (+100.000%) [+2.00000x]
RAM Hits: 22|20 (+10.0000%) [+1.10000x]
Total read+write: 338852|25005 (+1255.14%) [+13.5514x]
Estimated Cycles: 339608|25689 (+1222.00%) [+13.2200x]
Gungraun result: Ok. 1 without regressions; 0 regressed; 0 filtered; 1 benchmarks finished in 0.15254s

О, здорово! Gungraun показывает нам разницу между оценочными циклами наших игр FizzBuzz и FizzBuzzFibonacci. Ваши числа будут немного отличаться от моих. Однако разница между двумя играми, вероятно, находится в диапазоне 10-15x. Мне кажется, это хорошо! Особенно для добавления такой изысканной функции как Fibonacci в нашу игру. Детям понравится!

Расширяем FizzBuzzFibonacci на Rust

Наша игра стала хитом! Дети действительно любят играть в FizzBuzzFibonacci. Настолько сильно, что нам донеслись слухи от боссов, что они хотят создать продолжение. Но мы живем в современном мире, нам нужен ежегодный повторяющийся доход (ARR), а не разовые покупки! Новое видение нашей игры - это открытая игра, больше не нужно жить в пределах от 1 до 100 (даже если это включительно). Нет, мы открываем новые горизонты!

Правила для Open World FizzBuzzFibonacci следующие:

Напишите программу, которая принимает на ввод любое положительное целое число и выводит:

  • Для кратных трем, выводит Fizz
  • Для кратных пяти, выводит Buzz
  • Для кратных и трем, и пяти, выводит FizzBuzz
  • Для чисел, которые являются частью последовательности Фибоначчи, выводит только Fibonacci
  • Для всех остальных чисел, выводит само число

Чтобы наша игра работала с любым числом, нам нужно будет принять аргумент командной строки. Обновите функцию main так, чтобы она выглядела так:

fn main() {
let args: Vec<String> = std::env::args().collect();
let i = args
.get(1)
.map(|s| s.parse::<u32>())
.unwrap_or(Ok(15))
.unwrap_or(15);
play_game(i, true);
}
  • Собираем все аргументы (args), переданные нашей игре из командной строки.
  • Получаем первый аргумент, переданный нашей игре, и анализируем его как беззнаковое целое число i.
  • Если парсинг сфейлился или аргумент не был передан, по умолчанию считаем, что в нашу игру играют с 15 на входе.
  • Наконец, играем в нашу игру с новым разобранным беззнаковым целым числом i.

Теперь мы можем играть в нашу игру с любым числом! Используйте cargo run, затем -- для передачи аргументов нашей игре:

$ cargo run -- 9
Compiling playground v0.0.1 (/home/bencher)
Finished dev [unoptimized + debuginfo] target(s) in 0.44s
Running `target/debug/game 9`
Fizz
$ cargo run -- 10
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/game 10`
Buzz
$ cargo run -- 13
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/game 13`
Fibonacci

А если мы опустим или передадим недействительное число:

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/game`
FizzBuzz
$ cargo run -- bad
Finished dev [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/game bad`
FizzBuzz

Вау, это было детальное тестирование! CI проходит. Наши шефы в восторге. Давайте пустим это в продакшн! 🚀

Конец


SpongeBob SquarePants Three Weeks Later
This is Fine meme

🐰 … конец вашей карьеры, может быть?


Шутка ли, всё в огне! 🔥

Сначала казалось, что все идет нормально. Но в 02:07 утра в субботу мой пейджер прозвучал:

📟 Ваша игра в огне! 🔥

Выпрыгнув из кровати, я пытался понять, что происходит. Я попытался пройтись по логам, но это было сложно, потому что все постоянно вылетало. Наконец, я нашёл проблему. Дети! Им настолько понравилась наша игра, что они играли в нее аж до миллиона! В свете гениального озарения, я добавил два новых бенчмарка:

Вот где параметризованные бенчмарки Gungraun блистают! Вместо написания отдельных функций бенчмарков для каждого входного значения, мы можем использовать атрибут #[benches::...]:

#[library_benchmark]
#[benches::play(100, 1_000_000)]
fn bench_play_game(n: u32) {
play_game(black_box(n), black_box(false));
}
library_benchmark_group!(
name = bench_play_game_group,
benchmarks = [bench_play_game_100, bench_play_game]
);
  • Добавьте атрибут #[benches::play(100, 1_000_000)] для создания варианта бенчмарка со входом 100 и другого со входом 1_000_000.
  • Функция бенчмарка принимает параметр n: u32, который получает каждое значение.
  • Добавьте функцию bench_play_game в library_benchmark_group!

Отлично! Одна функция бенчмарка, несколько тестовых случаев!

Когда я запустил его, я получил:

$ cargo bench
Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s
Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)
play_game::bench_play_game_group::bench_play_game_100
Instructions: 331835|331835 (No change)
L1 Hits: 338831|338828 (+0.00089%) [+1.00001x]
LL Hits: 1|2 (-50.0000%) [-2.00000x]
RAM Hits: 20|22 (-9.09091%) [-1.10000x]
Total read+write: 338852|338852 (No change)
Estimated Cycles: 339536|339608 (-0.02120%) [-1.00021x]
play_game::bench_play_game_group::bench_play_game play_0:(100)
Instructions: 7072|N/A (*********)
L1 Hits: 7128|N/A (*********)
LL Hits: 1|N/A (*********)
RAM Hits: 9|N/A (*********)
Total read+write: 7138|N/A (*********)
Estimated Cycles: 7448|N/A (*********)
play_game::bench_play_game_group::bench_play_game play_1:(1_000_000)
Instructions: 183930316|N/A (*********)
L1 Hits: 183930372|N/A (*********)
LL Hits: 1|N/A (*********)
RAM Hits: 9|N/A (*********)
Total read+write: 183930382|N/A (*********)
Estimated Cycles: 183930692|N/A (*********)
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 1.45441s

Бенчмарк завершился за 1,45 секунды. Это было быстро! Вместо многократного запуска бенчмарков, как это делают бенчмарки реального времени, каждый бенчмарк Gungraun запускается только один раз. Но подождите, почему в первом бенчмарке bench_play_game_100 есть изменения, хотя мы ничего не меняли в этом бенчмарке? Верно, но мы изменили кое-что другое в файле бенчмарка, и поскольку Gungraun и Valgrind являются чувствительными инструментами, даже очень небольшие изменения регистрируются. Однако такие небольшие изменения, особенно в метриках кэша, пренебрежимо малы. Со временем вы разовьёте чутьё на критические изменения в метриках. Давайте рассмотрим наш вывод подробнее.

play_game::bench_play_game_group::bench_play_game play_1:(1_000_000)
Instructions: 183930316|N/A (*********)
L1 Hits: 183930372|N/A (*********)
LL Hits: 1|N/A (*********)
RAM Hits: 9|N/A (*********)
Total read+write: 183930382|N/A (*********)
Estimated Cycles: 183930692|N/A (*********)

Что! 7 448 оценочных циклов x 1 000 должно быть 7 448 000 оценочных циклов, а не 183 930 692 оценочных цикла 🤯 Хотя мой код последовательности Фибоначчи функционально верен, где-то у меня должна быть ошибка производительности.

Исправляем FizzBuzzFibonacci в Rust

Давайте еще раз взглянем на функцию is_fibonacci_number:

fn is_fibonacci_number(n: u32) -> bool {
for i in 0..=n {
let (mut previous, mut current) = (0, 1);
while current < i {
let next = previous + current;
previous = current;
current = next;
}
if current == n {
return true;
}
}
false
}

Теперь, когда я думаю о производительности, я понимаю, что у меня есть ненужный, дополнительный цикл. Мы можем полностью избавиться от цикла for i in 0..=n {} и просто сравнить значение current с данной числом (n) 🤦

fn is_fibonacci_number(n: u32) -> bool {
let (mut previous, mut current) = (0, 1);
while current < n {
let next = previous + current;
previous = current;
current = next;
}
current == n
}
  • Обновите вашу функцию is_fibonacci_number.
  • Инициализируйте последовательность Фибоначчи, начав с 0 и 1, как previous и current числах соответственно.
  • Итерируйте пока current число меньше данного числа n.
  • Добавьте previous и current число, чтобы получить next число.
  • Обновите число previous на число current.
  • Обновите число current на число next.
  • Как только current становится больше или равно данному числу n, мы выйдем из цикла.
  • Проверьте, равно ли current число данному числу n, и верните этот результат.

Теперь давайте повторно запустим эти бенчмарки и посмотрим, как мы справились:

$ cargo bench
Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s
Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)
play_game::bench_play_game_group::bench_play_game_100
Instructions: 23679|331835 (-92.8642%) [-14.0139x]
L1 Hits: 30675|338831 (-90.9468%) [-11.0458x]
LL Hits: 2|1 (+100.000%) [+2.00000x]
RAM Hits: 19|20 (-5.00000%) [-1.05263x]
Total read+write: 30696|338852 (-90.9412%) [-11.0390x]
Estimated Cycles: 31350|339536 (-90.7668%) [-10.8305x]
play_game::bench_play_game_group::bench_play_game play_0:(100)
Instructions: 218|7072 (-96.9174%) [-32.4404x]
L1 Hits: 273|7128 (-96.1700%) [-26.1099x]
LL Hits: 1|1 (No change)
RAM Hits: 10|9 (+11.1111%) [+1.11111x]
Total read+write: 284|7138 (-96.0213%) [-25.1338x]
Estimated Cycles: 628|7448 (-91.5682%) [-11.8599x]
play_game::bench_play_game_group::bench_play_game play_1:(1_000_000)
Instructions: 332|183930316 (-99.9998%) [ -554007x]
L1 Hits: 387|183930372 (-99.9998%) [ -475272x]
LL Hits: 1|1 (No change)
RAM Hits: 10|9 (+11.1111%) [+1.11111x]
Total read+write: 398|183930382 (-99.9998%) [ -462137x]
Estimated Cycles: 742|183930692 (-99.9996%) [ -247885x]
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 0.45459s

О, вау! Наш бенчмарк 100 снижается на 11%, а наш бенчмарк 1_000_000 снизился более чем в 200 000 раз! С 183 930 692 оценочных циклов до 742 оценочных циклов! Это снижение на 99,9996%!

🐰 Эй, по крайней мере мы поймали эту ошибку производительности до того, как она попала в продакшен… ох, точно. Неважно…

Отслеживание регрессий производительности в CI

Руководители были недовольны потоком отрицательных отзывов, которые наша игра получила из-за моей ошибки в производительности. Они сказали мне, чтобы это больше не происходило, и когда я спросил как, они просто сказали мне больше этого не делать. Как мне это контролировать‽

К счастью, я нашел этот замечательный инструмент с открытым исходным кодом под названием Bencher. У него есть очень щедрый бесплатный уровень, поэтому я могу использовать Bencher Cloud для своих личных проектов. А на работе, где все должно быть в нашем приватном облаке, я начал использовать Самостоятельный хостинг Bencher.

У Bencher есть встроенные адаптеры, поэтому их легко интегрировать в CI. После прочтения руководства по быстрому старту, я могу запускать свои бенчмарки и отслеживать их с помощью Bencher.

$ bencher run --project game "cargo bench"
Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s
Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)
play_game::bench_play_game_group::bench_play_game_100
Instructions: 23679|23679 (No change)
L1 Hits: 30675|30675 (No change)
LL Hits: 2|2 (No change)
RAM Hits: 19|19 (No change)
Total read+write: 30696|30696 (No change)
Estimated Cycles: 31350|31350 (No change)
play_game::bench_play_game_group::bench_play_game play_0:(100)
Instructions: 218|218 (No change)
L1 Hits: 273|273 (No change)
LL Hits: 1|1 (No change)
RAM Hits: 10|10 (No change)
Total read+write: 284|284 (No change)
Estimated Cycles: 628|628 (No change)
play_game::bench_play_game_group::bench_play_game play_1:(1_000_000)
Instructions: 332|332 (No change)
L1 Hits: 387|387 (No change)
LL Hits: 1|1 (No change)
RAM Hits: 10|10 (No change)
Total read+write: 398|398 (No change)
Estimated Cycles: 742|742 (No change)
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 0.45370s
Bencher New Report:
...
View results:
- play_game::bench_play_game_group::bench_play_game play_0:(100) (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_0:(100) (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_0:(100) (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_0:(100) (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_0:(100) (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_0:(100) (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game_100 (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game_100 (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game_100 (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game_100 (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game_100 (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409
- play_game::bench_play_game_group::bench_play_game_100 (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409

Используя это замечательное устройство для путешествий во времени, которое мне дал милый кролик, Я смог вернуться в прошлое и повторить то, что бы произошло, если бы мы использовали Bencher с самого начала. Вы можете увидеть, где мы впервые внесли ошибочную реализацию FizzBuzzFibonacci. Я немедленно получил ошибки в CI в виде комментария к моему запросу на вытягивание. В тот же день я исправил ошибку производительности, устранив не нужный, лишний цикл. Никаких пожаров. Только довольные пользователи.

Bencher: Непрерывное тестирование производительности

Bencher - это набор инструментов для непрерывного тестирования производительности. Когда-нибудь регрессия производительности влияла на ваших пользователей? Bencher мог бы предотвратить это. Bencher позволяет вам обнаруживать и предотвращать регрессии производительности до того, как они будут слиты.

  • Запустить: Запустите свои тесты производительности локально или в CI, используя точно те же самые bare metal раннеры и ваши любимые инструменты для тестирования. CLI bencher оркестрирует запуск ваших тестов производительности на bare metal и сохраняет их результаты.
  • Отслеживать: Отслеживайте результаты ваших тестов производительности со временем. Мониторите, запрашивайте и строите графики результатов с помощью веб-консоли Bencher на основе ветки исходного кода, испытательного стенда и меры.
  • Поймать: Отлавливайте регрессии производительности локально или в CI, используя точно то же самое bare metal оборудование. Bencher использует инструменты аналитики, работающие по последнему слову техники, чтобы обнаружить регрессии производительности, прежде чем они будут слиты.

По тем же причинам, по которым модульные тесты запускаются, чтобы предотвратить регрессии функций, тесты производительности должны быть запущены с Bencher, чтобы предотвратить регрессии производительности. Ошибки производительности – это тоже ошибки!

Начните отлавливать регрессии производительности — попробуйте Bencher Cloud бесплатно.

🤖 Этот документ был автоматически переведён с помощью ИИ. Он может быть неточным и содержать ошибки. Если вы обнаружите какие-либо ошибки, откройте проблему на GitHub.