Что такое 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)
Что делает: Специализированный метод для бенчмаркинга функций, которые принимают ссылку на данные. Позволяет избежать копирования данных на каждой итерации.
Параметры:
name: &str— имя.