Как правильно замерить производительность кода на 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, но без статистической обработки.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги