Avatar ·

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

Всем привет! Я новичок в Rust и пытаюсь понять, как правильно измерять производительность своих программ. В других языках (например, Python или JavaScript) я просто использовал встроенные таймеры или модули, но в Rust всё оказалось сложнее. Я написал простую функцию, которая суммирует элементы в векторе, и хочу узнать, сколько времени она выполняется: ```rust fn sum_vec(vec: &Vec) -> i32 { let mut sum = 0; for i in vec { sum += i; } sum } fn main() { let data = vec![1; 1_000_000]; // Как замерить время выполнения sum_vec(&data)? println!("Sum: {}", sum_vec(&data)); } ``` Я пробовал использовать `std::time::Instant`, но получаю странные результаты: иногда 0 микросекунд, иногда сильно различающиеся значения при повторных запусках. Ещё я слышал про `cargo bench`, но не понимаю, как его настроить. Подскажите, пожалуйста: 1. Как правильно замерить время выполнения небольшого фрагмента кода? 2. Как избежать влияния компиляторных оптимизаций (чтобы компилятор не выкинул «бесполезный» код)? 3. Есть ли готовые инструменты для профессионального бенчмаркинга в Rust? Заранее спасибо! Очень хочется разобраться, так как пишу библиотеку для работы с большими данными и производительность критична. <<>>

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

Ваша проблема типична для новичков: std::time::Instant даёт неточные результаты из-за оптимизаций компилятора и шума в измерениях. Рассмотрим три ключевых аспекта: защита от оптимизаций, усреднение замеров и профессиональные инструменты.

1. Использование std::time::Instant с защитой от оптимизаций

Компилятор Rust (LLVM) может полностью удалить код, результат которого не используется. Чтобы этого избежать, используйте std::hint::black_box (стабильно с Rust 1.66):

use std::time::Instant;
use std::hint::black_box;

fn sum_vec(vec: &[i32]) -> i32 {
    vec.iter().sum()
}

fn main() {
    let data = vec![1; 1_000_000];
    
    // Прогревочный запуск (убираем влияние кэша процессора)
    black_box(sum_vec(&data));
    
    let start = Instant::now();
    let result = black_box(sum_vec(&data));
    let duration = start.elapsed();
    
    println!("Sum: {}, Time: {:?}", result, duration);
}

black_box заставляет компилятор считать, что аргумент и результат используются, предотвращая оптимизацию. Также лучше передавать срез &[i32], а не &Vec<i32> — это более идиоматично.

2. Многократные замеры и статистика

Одиночный замер ненадёжен. Лучше выполнить много итераций и усреднить:

fn benchmark(f: fn(), iterations: u32) -> std::time::Duration {
    let mut total = std::time::Duration::new(0, 0);
    for _ in 0..iterations {
        let start = Instant::now();
        black_box(f());
        total += start.elapsed();
    }
    total / iterations
}

fn main() {
    let data = vec![1; 1_000_000];
    let closure = || sum_vec(&data);
    
    // Прогрев
    for _ in 0..10 { black_box(closure()); }
    
    let avg = benchmark(closure, 1000);
    println!("Average time: {:?}", avg);
}

3. Профессиональный инструмент: criterion

Для серьёзных проектов используйте крейт criterion. Он автоматически делает прогревы, усредняет, строит графики и устойчив к шуму.

Установка в 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 sum_vec(vec: &[i32]) -> i32 {
    vec.iter().sum()
}

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

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

Запуск: cargo bench. Вы получите:

  • Среднее время выполнения
  • Доверительные интервалы (99%)
  • График распределения времени в формате HTML
  • Сравнение с предыдущими запусками (регрессионное тестирование)

Почему ваш код давал 0 микросекунд?

Без black_box компилятор видит, что результат sum_vec не используется, и полностью удаляет вызов. С Instant это даёт время, близкое к нулю. Прогрев и усреднение решают проблему шума.

Совет: для микробенчмарков (функции, выполняющиеся < 1 мкс) всегда используйте criterion — он корректно измеряет даже наносекунды.

<<>>

Альтернативный подход: встроенный test с атрибутом #[bench]

Если вы не хотите подключать внешние крейты, Rust предоставляет нестабильный API для бенчмаркинга через #![feature(test)]. Этот метод требует nightly-версии компилятора.

В Cargo.toml добавьте:

[lib]
bench = false  # отключаем стандартный bench

[[bench]]
name = "my_bench"
harness = false  # используем свой бенчмарк

Файл benches/my_bench.rs:

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

use test::Bencher;

#[bench]
fn bench_sum_vec(b: &mut Bencher) {
    let data = vec![1; 1_000_000];
    b.iter(|| {
        let _ = data.iter().sum::<i32>();
    });
}

Запуск: cargo +nightly bench. Вы получите:

  • Количество итераций (автоматически подбирается)
  • Среднее время на итерацию
  • Доверительный интервал

Недостатки:

  • Требуется nightly Rust (нестабильный канал)
  • Нет графиков и сравнения с предыдущими запусками
  • Меньше настроек, чем у criterion

Когда использовать: Если вы уже используете nightly для других целей и нужен быстрый замер без установки дополнительных крейтов. Для production-проектов лучше выбрать criterion.

Важно: Не забудьте про black_box — в этом API он встроен в Bencher, но можно дополнительно обернуть данные.