Как замерять производительность в Rust: бенчмаркинг для новичков

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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);

Блоги

Рекомендаций по книгам