Почему Rust требует особого подхода к бенчмаркингу?
Rust славится своей скоростью, но даже самый быстрый язык можно замедлить неудачной реализацией. Вы когда-нибудь писали алгоритм на Rust, думая: «Это будет молниеносно!», а потом запускали и не понимали — где потерялись микросекунды? Знакомо?
Бенчмаркинг в Rust — это не просто запуск таймера и замер времени. Это целая наука: нужно учитывать оптимизации компилятора, кэширование процессора, и даже то, как Rust управляет памятью. В этой статье мы разберем как правильно бенчмаркать программы на Rust, чтобы вы получали честные цифры, а не случайные результаты.
Мы пройдем путь от простых замеров с std::time до профессионального инструмента criterion, который используют в реальных проектах. Поехали!
1. Самый простой способ: std::time::Instant
Если вам нужно быстро «пощупать» производительность куска кода — без установки крейтов, просто вставьте замер прямо в main. Но есть подвох: оптимизатор Rust может «выкинуть» ваш код, если он не использует результат.
use std::time::Instant;
fn main() {
let start = Instant::now();
// Ваш код, который нужно замерить
let mut sum = 0u64;
for i in 0..1_000_000 {
sum += i;
}
let duration = start.elapsed();
println!("Время выполнения: {:?}", duration);
// Обязательно используйте результат!
println!("Сумма: {}", sum);
}
Почему нельзя просто замерить один запуск?
Один запуск — это лотерея. Система может быть занята, процессор троттлит, кэш холодный. Правильный бенчмарк в Rust должен:
- Прогревать код (warm-up) — несколько итераций до замера
- Запускать код много раз (тысячи итераций)
- Усреднять результаты
Ручная реализация этого — путь к ошибкам. Поэтому сообщество Rust придумало criterion.
2. Профессиональный бенчмаркинг: крейт Criterion
Criterion.rs — это стандарт де-факто для бенчмаркинга в Rust. Он используется в самом компиляторе rustc и в сотнях крейтов. Почему он крут?
- Автоматический прогрев и статистическая обработка
- Графики и сравнение с предыдущими запусками
- Обнаружение регрессий (если код стал медленнее — вы узнаете)
- Поддержка параметризованных бенчмарков
Установка и настройка
Добавьте в Cargo.toml:
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "my_benchmark"
harness = false
Создайте файл benches/my_benchmark.rs:
use criterion::{black_box, criterion_group, criterion_main, Criterion};
// Функция, которую бенчмаркаем
fn fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fibonacci 20", |b| {
// black_box — запрещает оптимизатору выкинуть код
b.iter(|| fibonacci(black_box(20)))
});
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
Запуск: cargo bench. Вы увидите что-то вроде:
fibonacci 20 time: [24.567 µs 24.789 µs 25.012 µs]
Три числа — это доверительный интервал (нижняя, средняя, верхняя граница). Если интервал узкий — бенчмарк стабилен.
3. Секретный ингредиент: black_box
Вы могли заметить black_box в примере. Это магическая функция из criterion, которая запрещает LLVM оптимизировать аргументы или результат. Без неё ваш бенчмарк может показать 0 нс — потому что компилятор просто вычислил константу на этапе компиляции.
Пример плохого бенчмарка:
// Компилятор может заменить это на "return 42"!
c.bench_function("bad", |b| b.iter(|| 2 + 2));
Используйте black_box везде, где данные приходят извне или результат куда-то идёт.
4. Сравнение двух алгоритмов
Часто нужно понять: какой из двух подходов быстрее? Criterion позволяет сравнивать их в одном бенчмарке:
fn bench_comparison(c: &mut Criterion) {
let mut group = c.benchmark_group("sorting");
group.bench_function("bubble_sort", |b| {
b.iter(|| bubble_sort(black_box(&mut vec![5, 3, 1, 4, 2])))
});
group.bench_function("quick_sort", |b| {
b.iter(|| quick_sort(black_box(&mut vec![5, 3, 1, 4, 2])))
});
group.finish();
}
После запуска вы получите красивый HTML-отчёт в target/criterion/report/index.html с графиками и процентным сравнением.
5. Бенчмаркинг с параметрами
Иногда нужно проверить, как алгоритм ведёт себя на разных размерах данных. Criterion поддерживает параметризованные бенчмарки:
use criterion::{Criterion, BenchmarkId};
fn bench_from_elem(c: &mut Criterion) {
static SIZES: [usize; 4] = [100, 1000, 10_000, 100_000];
let mut group = c.benchmark_group("Vec::from_elem");
for size in SIZES.iter() {
group.bench_with_input(
BenchmarkId::new("size", size),
size,
|b, &size| b.iter(|| vec![0u8; size]),
);
}
group.finish();
}
Это генерирует отдельные замеры для каждого размера — вы сразу видите, где начинается квадратичный рост.
6. Бенчмаркинг асинхронного кода
Rust активно используется в async-среде (tokio, async-std). Бенчмаркать async-функции чуть сложнее — нужен runtime. Criterion имеет специальную поддержку:
use criterion::{criterion_group, criterion_main, Criterion};
use tokio::runtime::Runtime;
async fn async_work() -> u64 {
// какая-то асинхронная работа
42
}
fn bench_async(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("async_example", |b| {
b.to_async(&rt).iter(|| async_work())
});
}
criterion_group!(benches, bench_async);
criterion_main!(benches);