criterion для Rust: полное руководство с примерами

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

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

Начать курс

Библиотека Criterion для Rust: статистический бенчмаркинг

В мире системного программирования производительность — это не просто пожелание, а часто жесткое требование. Rust, будучи языком, который дает разработчику контроль над памятью и временем выполнения, требует инструментов, способных точно измерить этот контроль. Здесь на сцену выходит Criterion — библиотека для статистического бенчмаркинга, которая превращает процесс измерения производительности из грубого "секундомера" в научный эксперимент.

Зачем нужна эта библиотека? Стандартный бенчмаркинг в Rust (через #[bench]) полезен, но он имеет фундаментальный недостаток: он не учитывает статистический разброс. Современные процессоры имеют сложную архитектуру с кэшами, предсказателями переходов и турбо-бустом. Один и тот же код может выполняться за разное время в зависимости от "шума" операционной системы, температуры CPU или случайного состояния кэша. Criterion решает эту проблему, запуская каждый тест множество раз, собирая выборку времени выполнения и применяя к ней статистические методы (среднее, медиана, стандартное отклонение, процентили). Результат — не просто "время в микросекундах", а доверительный интервал, который говорит: "С вероятностью 95% истинное время выполнения лежит в диапазоне от X до Y".

Кроме того, Criterion предоставляет мощные инструменты для регрессионного анализа. Вы можете сравнить производительность двух версий вашего кода (например, до и после оптимизации) и получить статистически значимый ответ на вопрос: "Стала ли новая версия быстрее, или разница в пределах погрешности?". Библиотека генерирует красивые HTML-отчеты с графиками, гистограммами и таблицами, что делает анализ производительности наглядным и убедительным. Это незаменимый инструмент для разработчиков библиотек, игровых движков, баз данных, криптографических алгоритмов и любого другого кода, где каждая наносекунда на счету.

Установка

Установка Criterion в проект Rust выполняется через менеджер пакетов cargo. Вам нужно добавить зависимость в файл Cargo.toml вашего проекта. Рекомендуется добавлять её как dev-зависимость, так как бенчмарки не являются частью конечного приложения.

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

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

Пояснение:

  • version = "0.5" — указывает версию библиотеки. На момент написания статьи актуальна версия 0.5.x.
  • features = ["html_reports"] — включает генерацию HTML-отчетов с графиками. Это опциональная, но очень полезная функция.
  • [[bench]] — секция, которая говорит Cargo, что у нас есть бенчмарк с именем my_benchmark.
  • harness = false — отключает встроенный бенчмарк-раннер Cargo. Это необходимо, чтобы Criterion мог взять управление на себя.

После этого создайте файл benches/my_benchmark.rs (путь должен соответствовать имени, указанному в [[bench]]).

Быстрый старт: минимальный рабочий пример

Давайте напишем простейший бенчмарк, который измеряет скорость вычисления суммы чисел от 1 до 1000.

// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn sum_up_to(n: u64) -> u64 {
    let mut sum = 0;
    for i in 1..=n {
        sum += i;
    }
    sum
}

fn bench_sum(c: &mut Criterion) {
    // black_box предотвращает оптимизацию компилятора
    c.bench_function("sum 1000", |b| b.iter(|| sum_up_to(black_box(1000))));
}

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

Как запустить:

cargo bench

После выполнения в консоли вы увидите что-то вроде:

sum 1000                 time:   [1.2345 µs 1.2456 µs 1.2567 µs]
Found 1 outliers among 100 measurements (1.00%)

А в директории target/criterion появится HTML-отчет с детальной статистикой и графиками.

Основные методы и функции

Рассмотрим ключевые компоненты библиотеки Criterion. Каждый из них играет важную роль в построении качественных бенчмарков.

1. Criterion::bench_function

Сигнатура: pub fn bench_function<F>(&mut self, name: &str, f: F) where F: FnMut(&mut Bencher)

Что делает: Регистрирует и запускает один бенчмарк с заданным именем. Это основной метод для создания простых тестов производительности.

use criterion::{Criterion, black_box};

fn bench_addition(c: &mut Criterion) {
    c.bench_function("add two numbers", |b| {
        b.iter(|| black_box(2 + 2))
    });
}

2. Bencher::iter

Сигнатура: pub fn iter<R, F>(&mut self, f: F) where F: FnMut() -> R

Что делает: Выполняет замыкание многократно (тысячи или миллионы раз), замеряя общее время. Это "сердце" каждого бенчмарка. Замыкание должно быть идемпотентным (выполняться без побочных эффектов, влияющих на последующие вызовы).

use criterion::{Criterion, Bencher, black_box};

fn bench_vec_push(c: &mut Criterion) {
    c.bench_function("vec push", |b: &mut Bencher| {
        b.iter(|| {
            let mut v = Vec::new();
            for i in 0..100 {
                v.push(black_box(i));
            }
        });
    });
}

3. black_box

Сигнатура: pub fn black_box<T>(dummy: T) -> T

Что делает: Критически важная функция, которая "обманывает" оптимизатор компилятора. Без неё LLVM может вычислить результат константного выражения на этапе компиляции и заменить вызов функции на готовое число, что сделает бенчмарк бессмысленным. black_box заставляет компилятор считать, что значение используется где-то ещё, предотвращая преждевременную оптимизацию.

use criterion::{black_box, Criterion};

fn bench_with_black_box(c: &mut Criterion) {
    let data = vec![1, 2, 3, 4, 5];
    c.bench_function("sum with black_box", |b| {
        b.iter(|| {
            let sum: i32 = black_box(&data).iter().sum();
            black_box(sum) // Возвращаем результат через black_box
        })
    });
}

4. Criterion::bench_with_input

Сигнатура: pub fn bench_with_input<F, I>(&mut self, name: &str, input: &I, f: F) where F: FnMut(&mut Bencher, &I)

Что делает: Позволяет передать в бенчмарк внешние данные (например, строку для парсинга или массив для сортировки). Это удобно, когда подготовка данных дороже самого теста, и вы хотите вынести её за пределы цикла iter.

use criterion::{Criterion, black_box};

fn bench_parsing(c: &mut Criterion) {
    let input = "12345.6789";
    c.bench_with_input("parse float", &input, |b, i| {
        b.iter(|| black_box(i).parse::().unwrap())
    });
}

5. Criterion::bench_function_over_inputs

Сигнатура: pub fn bench_function_over_inputs<F, I>(&mut self, name: &str, inputs: &[I], f: F) where F: FnMut(&mut Bencher, &I)

Что делает: Запускает один и тот же бенчмарк для набора различных входных данных. Результаты для каждого входа будут отображены отдельно, что позволяет увидеть, как производительность масштабируется с размером данных.

use criterion::{Criterion, black_box};

fn bench_sort(c: &mut Criterion) {
    let sizes = vec![10, 100, 1000];
    c.bench_function_over_inputs("sort", |b, &size| {
        let mut data: Vec = (0..size).rev().collect();
        b.iter(|| {
            data.sort();
            black_box(&data);
            // Восстанавливаем порядок для следующей итерации
            data.reverse();
        });
    }, &sizes);
}

6. Criterion::criterion_group! и Criterion::criterion_main!

Сигнатура (макросы): criterion_group!(name, bench1, bench2, ...); и criterion_main!(name);

Что делает: Эти макросы являются точкой входа для Criterion. criterion_group! объединяет несколько функций-бенчмарков в группу. criterion_main! генерирует функцию main, которая запускает все указанные группы. Без них бенчмарки не будут выполнены.

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

fn bench_one(c: &mut Criterion) { /* ... */ }
fn bench_two(c: &mut Criterion) { /* ... */ }

criterion_group!(benches, bench_one, bench_two);
criterion_main!(benches);

7. Criterion::throughput

Сигнатура: pub fn throughput(&mut self, throughput: Throughput) -> &mut Self

Что делает: Устанавливает метрику пропускной способности для бенчмарка. Вы можете указать количество байт или элементов, обрабатываемых за одну итерацию. Criterion автоматически рассчитает и отобразит скорость (например, MiB/s или элементы/сек).

use criterion::{Criterion, Throughput, black_box};

fn bench_memcpy(c: &mut Criterion) {
    let data = vec![0u8; 1024 * 1024]; // 1 MiB
    let mut group = c.benchmark_group("memcpy");
    group.throughput(Throughput::Bytes(data.len() as u64));
    group.bench_function("copy 1MiB", |b| {
        let mut dest = vec![0u8; data.len()];
        b.iter(|| {
            dest.copy_from_slice(black_box(&data));
        });
    });
    group.finish();
}

8. Criterion::benchmark_group

Сигнатура: pub fn benchmark_group(&mut self, name: &str) -> BenchmarkGroup

Что делает: Создает группу бенчмарков с общими настройками (например, throughput, конфигурация выборки). Это позволяет избежать дублирования кода и задать параметры один раз для нескольких связанных тестов.

use criterion::{Criterion, BenchmarkGroup, measurement::WallTime};

fn bench_group_example(c: &mut Criterion) {
    let mut group: BenchmarkGroup<WallTime> = c.benchmark_group("sorting");
    group.sample_size(50); // Уменьшаем количество измерений для скорости
    group.bench_function("bubble", |b| b.iter(|| /* bubble sort */));
    group.bench_function("quick", |b| b.iter(|| /* quick sort */));
    group.finish(); // Обязательно завершаем группу
}

9. BenchmarkGroup::sample_size

Сигнатура: pub fn sample_size(&mut self, n: usize) -> &mut Self

Что делает: Устанавливает количество измерений (сэмплов), которые Criterion сделает для каждого бенчмарка в группе. По умолчанию — 100. Увеличение числа повышает точность, но замедляет выполнение бенчмарков.

use criterion::{Criterion, BenchmarkGroup};

fn bench_with_custom_samples(c: &mut Criterion) {
    let mut group = c.benchmark_group("precise");
    group.sample_size(200); // Делаем 200 замеров для большей точности
    group.bench_function("calc", |b| b.iter(|| 2 + 2));
    group.finish();
}

10. BenchmarkGroup::measurement_time

Сигнатура: pub fn measurement_time(&mut self, duration: Duration) -> &mut Self

Что делает: Устанавливает минимальное время, которое Criterion потратит на сбор измерений для каждого бенчмарка. По умолчанию — 5 секунд. Если бенчмарк очень быстрый, за это время будет сделано много итераций. Если медленный — мало. Это гарантирует, что даже для медленных тестов будет собрано достаточно данных.

use std::time::Duration;
use criterion::{Criterion, BenchmarkGroup};

fn bench_long_running(c: &mut Criterion) {
    let mut group = c.benchmark_group("slow");
    group.measurement_time(Duration::from_secs(10)); // Собираем данные 10 секунд
    group.bench_function("slow_fn", |b| b.iter(|| std::thread::sleep(Duration::from_millis(1))));
    group.finish();
}
                
    
    

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