Avatar ·

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

Привет, коллеги! Я пишу на Rust около полугода и столкнулся с проблемой: не могу нормально замерить, сколько времени выполняется мой код. Пытался использовать std::time::Instant, но результаты прыгают от запуска к запуску, особенно на маленьких функциях. Например, я хочу сравнить два способа суммирования вектора: ```rust use std::time::Instant; fn sum_for(v: &[i32]) -> i32 { let mut sum = 0; for x in v { sum += x; } sum } fn sum_iter(v: &[i32]) -> i32 { v.iter().sum() } fn main() { let data = vec![1; 1_000_000]; let start = Instant::now(); let _ = sum_for(&data); println!("for: {:?}", start.elapsed()); let start = Instant::now(); let _ = sum_iter(&data); println!("iter: {:?}", start.elapsed()); } ``` На моём ноутбуке (Ryzen 5, Linux) первый замер показывает 0.5-1.2 мс, второй — 0.3-0.9 мс, причём порядок иногда меняется. Я понимаю, что это из-за фоновых процессов, кэша и т.д., но как получить стабильные цифры? Слышал про criterion, но не знаю, как его правильно подключить и интерпретировать результаты. Может, есть встроенные средства в Rust? Ещё интересно, как замерить потребление памяти или найти узкие места, а не только время. Подскажите, пожалуйста, best practices для бенчмаркинга в Rust. <<>>

Правильный бенчмаркинг в Rust: от простого к продвинутому

Ваша проблема типична для новичков: ручные замеры через std::time::Instant дают шум из-за планировщика ОС, кэша процессора, троттлинга CPU и компиляторных оптимизаций. Rust предоставляет несколько уровней инструментов для точного измерения производительности.

1. Встроенный тестовый бенчмаркинг (nightly)

Если вы используете nightly-канал Rust, можно задействовать #[bench] атрибут. Добавьте в Cargo.toml:

[[bench]]
name = "my_bench"
harness = false

Создайте файл benches/my_bench.rs:

#![feature(test)]
extern crate test;

use test::Bencher;

#[bench]
fn bench_sum_for(b: &mut Bencher) {
    let data = vec![1; 1_000_000];
    b.iter(|| sum_for(&data));
}

#[bench]
fn bench_sum_iter(b: &mut Bencher) {
    let data = vec![1; 1_000_000];
    b.iter(|| sum_iter(&data));
}

Запуск: cargo bench. Bencher автоматически прогревает код, делает множество итераций и выдаёт стабильные наносекунды на операцию. Но nightly нестабилен, поэтому для production чаще используют criterion.

2. Criterion — золотой стандарт

Добавьте в Cargo.toml:

[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "my_bench"
harness = false

В benches/my_bench.rs:

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn criterion_benchmark(c: &mut Criterion) {
    let data = vec![1; 1_000_000];
    
    c.bench_function("sum_for", |b| {
        b.iter(|| sum_for(black_box(&data)))
    });
    
    c.bench_function("sum_iter", |b| {
        b.iter(|| sum_iter(black_box(&data)))
    });
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

Ключевые моменты:

  • black_box — предотвращает оптимизацию компилятора, которая может выкинуть ваш код как "мёртвый".
  • Criterion выполняет прогревочные итерации, измеряет время с микросекундной точностью и строит графики (HTML-отчёт в target/criterion).
  • Результат: "time: [1.234 ms 1.345 ms 1.456 ms]" — доверительный интервал 95%.

3. Профилирование памяти и узких мест

Для анализа аллокаций используйте dhat (крэйт для heap-профилирования):

#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;

fn main() {
    let _profiler = dhat::Profiler::new_heap();
    // ваш код
    // после завершения — смотрим статистику
}

Для CPU-профилирования под Linux: perf record ./target/release/my_program + perf report. Для Windows — WPR/WPA. Есть также flamegraph крэйт, который строит флейм-графы.

4. Практические советы

  • Всегда компилируйте в release: cargo bench автоматически использует --release.
  • Изолируйте бенчмарк: не делайте замеры в main() — фоновые процессы влияют.
  • Используйте black_box: чтобы компилятор не выкинул результат.
  • Сравнивайте версии: Criterion умеет сохранять историю и показывать регрессии.
  • Для маленьких функций (менее 100 нс) — увеличьте количество итераций или используйте Bencher::iter_batched.

Пример полного вывода criterion:

sum_for                 time:   [1.234 ms 1.256 ms 1.289 ms]
                        change: [-0.32% +0.12% +0.54%] (p = 0.78 > 0.05)
                        No change in performance detected.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild

Это означает, что разброс мал, а изменение статистически незначимо.

Итог

Для серьёзного бенчмаркинга используйте criterion — он решает все проблемы с шумом, оптимизациями и статистикой. Для быстрых проверок — cargo bench с nightly. Профилирование памяти — dhat, CPU — perf/flamegraph. Избегайте ручных замеров через Instant в production-коде.

<<>>

Альтернативный подход: микро-бенчмаркинг с помощью cargo-criterion и визуализация

Если вам нужна ещё более детальная статистика и автоматическая генерация отчётов, попробуйте cargo-criterion — это расширение для Cargo, которое запускает criterion с дополнительными опциями.

Установка: cargo install cargo-criterion. Затем запуск: cargo criterion. Отличия от обычного cargo bench:

  • Автоматически строит HTML-отчёты с графиками (гистограммы, тренды).
  • Поддерживает сравнение с baseline: cargo criterion --baseline v1.0.
  • Умеет сохранять результаты в JSON для кастомной обработки.

Для ещё более точных замеров на уровне наносекунд можно использовать pixie — крэйт, который использует аппаратные счётчики CPU (PMC). Пример:

use pixie::Pixie;

let pixie = Pixie::new();
pixie.start();
// ваш код
let counters = pixie.stop();
println!("Cycles: {}, Instructions: {}", counters.cycles, counters.instructions);

Это позволяет считать IPC (instructions per cycle) и выявлять узкие места конвейера процессора. Однако такой подход требует root-прав и специфичен для архитектуры x86.

Для тех, кто хочет минимум зависимостей: можно использовать std::hint::black_box (стабилизирован с Rust 1.66) вместо criterion-версии — это работает и в stable Rust, но без статистической обработки.