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

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

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

Начать курс

Что такое Criterion и зачем он нужен?

Criterion.rs — это мощная и гибкая библиотека для бенчмаркинга (тестирования производительности) на языке Rust. Она является прямым аналогом популярной библиотеки Criterion для Haskell, но адаптирована под экосистему Rust.

Зачем нужен Criterion? В отличие от простых замеров времени с помощью std::time::Instant, Criterion предоставляет:

  • Статистический анализ: Автоматически запускает код множество раз, собирает выборку времени выполнения и вычисляет среднее, медиану, стандартное отклонение и процентили.
  • Сравнение с эталоном: Позволяет сравнивать производительность текущей версии кода с предыдущей (регрессионное тестирование).
  • Генерация отчетов: Создает красивые HTML-отчеты с графиками и таблицами.
  • Параметрический бенчмаркинг: Легко тестировать производительность функции с разными входными данными.
  • Устойчивость к шуму: Алгоритмы Criterion устойчивы к флуктуациям времени выполнения, вызванным фоновыми процессами ОС.

Библиотека незаменима для разработчиков, которые хотят:

  • Оптимизировать критически важные участки кода.
  • Убедиться, что новый код не замедляет старый функционал.
  • Принимать решения на основе данных, а не догадок.

Установка Criterion в Rust-проект

В отличие от Python (pip) или Node.js (npm), в Rust установка библиотек происходит через менеджер пакетов Cargo. Criterion — это не библиотека для основного кода, а инструмент для тестирования, поэтому его добавляют в раздел [dev-dependencies] файла Cargo.toml.

Также вам понадобится создать каталог benches/ в корне вашего проекта (рядом с src/).

Шаг 1: Добавьте зависимость в Cargo.toml

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

Примечание: Флаг features = ["html_reports"] включает генерацию HTML-отчетов. Без него будут только текстовые выводы в консоль.

Шаг 2: Создайте файл бенчмарка

Создайте файл benches/my_benchmark.rs (имя может быть любым).

Шаг 3: Запустите бенчмарк

cargo bench

Эта команда скомпилирует ваш проект в режиме release (с оптимизациями) и запустит все бенчмарки из папки benches/.

Быстрый старт: первый бенчмарк

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

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

// Функция, которую мы будем тестировать
fn sum_slow(n: u64) -> u64 {
    let mut sum = 0;
    for i in 0..n {
        sum += i;
    }
    sum
}

fn sum_fast(n: u64) -> u64 {
    n * (n - 1) / 2
}

fn criterion_benchmark(c: &mut Criterion) {
    // Группируем бенчмарки
    let mut group = c.benchmark_group("sum_comparison");
    
    group.bench_function("slow", |b| b.iter(|| sum_slow(black_box(100))));
    group.bench_function("fast", |b| b.iter(|| sum_fast(black_box(100))));
    
    group.finish();
}

// Макросы для регистрации и запуска
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

Что здесь происходит?

  • black_box() — сообщает компилятору, что значение должно быть передано "как есть", предотвращая преждевременную оптимизацию (компилятор может вычислить константу на этапе компиляции, что исказит результат).
  • b.iter() — основной цикл бенчмарка. Criterion будет вызывать эту замыкание множество раз и замерять время.
  • criterion_group! и criterion_main! — макросы, которые регистрируют функцию бенчмарка и создают точку входа.

После запуска cargo bench вы увидите в консоли что-то вроде:

sum_comparison/slow   time:   [22.345 ns 22.567 ns 22.812 ns]
sum_comparison/fast   time:   [1.2345 ns 1.3456 ns 1.4567 ns]
Found 1 outliers among 100 measurements (1.00%)
  ...

А в папке target/criterion/ появится HTML-отчет.

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

Рассмотрим ключевые методы библиотеки. Все примеры предполагают, что у вас есть c: &mut Criterion.

1. c.bench_function(name, closure)

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

Параметры:

  • name: &str — уникальное имя бенчмарка.
  • closure: impl FnMut(&mut Bencher) — замыкание, которое получает объект Bencher. Внутри замыкания вызывается b.iter().

Пример:

c.bench_function("fibonacci 20", |b| b.iter(|| fibonacci(black_box(20))));

2. c.benchmark_group(name)

Что делает: Создает группу для логического объединения связанных бенчмарков. Группы отображаются вместе в отчетах.

Параметры:

  • name: &str — имя группы.

Возвращает: BenchmarkGroup, у которого есть методы .bench_function() и .finish().

Пример:

let mut group = c.benchmark_group("sorting");
group.bench_function("bubble_sort", |b| b.iter(|| bubble_sort(&mut data)));
group.bench_function("quick_sort", |b| b.iter(|| quick_sort(&mut data)));
group.finish();

3. c.bench_with_input(name, input, closure)

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

Параметры:

  • name: &str — имя.
  • input: &I — ссылка на входные данные.
  • closure: impl FnMut(&mut Bencher, &I) — замыкание, получающее Bencher и ссылку на входные данные.

Пример:

let input = vec![1, 2, 3, 4, 5];
c.bench_with_input("sum_of_vec", &input, |b, i| b.iter(|| sum_vec(i)));

4. c.bench_function_over_inputs(name, closure, inputs)

Что делает: Запускает один и тот же бенчмарк для списка различных входных значений. Автоматически генерирует имена вида name/input_value.

Параметры:

  • name: &str — базовое имя.
  • closure: impl FnMut(&mut Bencher, &I) — замыкание.
  • inputs: &[I] — срез входных данных.

Пример:

let sizes = [10, 100, 1000, 10000];
c.bench_function_over_inputs("factorial", |b, &size| b.iter(|| factorial(size)), &sizes);

5. black_box(value)

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

Параметры:

  • value: T — любое значение.

Возвращает: T — то же самое значение.

Пример:

let x = black_box(42);
let result = expensive_function(x); // Компилятор не сможет заменить x на константу 42

6. Bencher::iter(closure)

Что делает: Основной метод объекта Bencher. Запускает переданное замыкание в цикле и замеряет время его выполнения.

Параметры:

  • closure: impl FnMut() — код, время выполнения которого нужно измерить.

Пример:

|b| b.iter(|| {
    let result = my_function();
    black_box(result); // Чтобы компилятор не выкинул результат
})

7. Bencher::iter_custom(iterations, closure)

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

Параметры:

  • iterations: u64 — количество итераций.
  • closure: impl FnMut() — замыкание.

Пример:

|b| b.iter_custom(1000, || {
    let start = std::time::Instant::now();
    my_function();
    start.elapsed()
})

8. criterion_group!(name, functions)

Что делает: Макрос, который создает группу бенчмарков. Обязателен для регистрации функций.

Параметры:

  • name: ident — имя группы (будет использовано в criterion_main!).
  • functions: fn(&mut Criterion) — одна или несколько функций, принимающих &mut Criterion.

Пример:

criterion_group!(benches, bench_math, bench_string);
criterion_main!(benches);

9. criterion_main!(groups)

Что делает: Макрос, который создает функцию main() для запуска всех зарегистрированных групп.

Параметры:

  • groups: ident — одна или несколько групп, созданных через criterion_group!.

Пример:

criterion_main!(benches);

10. c.bench_ref_function(name, closure)

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

Параметры:

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