Оценка настроек компилятора Rust с помощью Criterion
Оценка производительности компилятора Rust с использованием Criterion
Контроль критерия с помощью скриптов и переменных окружения
В этой статье объясняется, во-первых, как провести тестирование с помощью популярного пакета criterion. Далее показывается, как сравнивать результаты тестирования при разных настройках компилятора. Хотя каждая комбинация настроек компилятора требует повторной компиляции и отдельного запуска, мы все равно можем составить таблицу и проанализировать результаты. Эта статья является дополнением к статье “Девять правил для ускорения SIMD в вашем коде на Rust” на ресурсе Towards Data Science.
Мы применили эту технику к пакету range-set-blaze
. Нашей целью является измерение эффекта производительности различных настроек SIMD (Single Instruction, Multiple Data). Мы также хотим сравнить производительность на разных процессорах. Такой подход также полезен для понимания преимуществ разных уровней оптимизации.
В рамках пакета range-set-blaze
, мы оцениваем:
- 3 уровня расширения SIMD —
sse2
(128 бит),avx2
(256 бит),avx512f
(512 бит) - 10 типов элементов —
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,isize
,usize
- 5 номеров канала — 4, 8, 16, 32, 64
- 2 процессора — AMD 7950X с расширением
avx512f
, Intel i5–8250U с расширениемavx2
- 5 алгоритмов — Regular, Splat0, Splat1, Splat2, Rotate
- 4 длины входных данных — 1024; 10 240; 102 400; 1 024 000
Из них первые четыре переменные (уровень расширения SIMD, тип элемента, номер канала, процессор) можно настраивать внешне. Последние две переменные (алгоритм и длина входных данных) мы контролируем с помощью циклов внутри обычного бенчмарк-кода на Rust.
- Сколько данных нам нужно? Балансировка машинного обучения и вопросов безопасности
- Меры по кибербезопасности для предотвращения отравления данных
- The translation result is ‘ELS+ Stream Tool
Начало работы с Criterion
Для добавления бенчмарков в ваш проект добавьте эту зависимость для разработки и создайте подпапку:
cargo add criterion --dev --features html_reportsmkdir benches
В файле Cargo.toml
добавьте:
[[bench]]name = "bench"harness = false
Создайте файл benches/bench.rs
. Вот пример кода:
#![feature(portable_simd)]#![feature(array_chunks)]use criterion::{black_box, criterion_group, criterion_main, Criterion};use is_consecutive1::*;// создаем строку, используя расширение SIMDconst SIMD_SUFFIX: &str = if cfg!(target_feature = "avx512f") { "avx512f,512"} else if cfg!(target_feature = "avx2") { "avx2,256"} else if cfg!(target_feature = "sse2") { "sse2,128"} else { "error"};type Integer = i32;const LANES: usize = 64;// сравниваем с этим#[inline]pub fn is_consecutive_regular(chunk: &[Integer; LANES]) -> bool { for i in 1..LANES { if chunk[i - 1].checked_add(1) != Some(chunk[i]) { return false; } } true}// определяем бенчмарк с названием "simple"fn simple(c: &mut Criterion) { let mut group = c.benchmark_group("simple"); group.sample_size(1000); // генерируем около 1 миллиона выровненных элементов let parameter: Integer = 1_024_000; let v = (100..parameter + 100).collect::<Vec<_>>(); let (prefix, simd_chunks, reminder) = v.as_simd::<LANES>(); // сохраняем выровненную часть let v = &v[prefix.len()..v.len() - reminder.len()]; // сохраняем выровненную часть group.bench_function(format!("regular,{}", SIMD_SUFFIX), |b| { b.iter(|| { let _: usize = black_box( v.array_chunks::<LANES>() .map(|chunk| is_consecutive_regular(chunk) as usize) .sum(), ); }); }); group.bench_function(format!("splat1,{}", SIMD_SUFFIX), |b| { b.iter(|| { let _: usize = black_box( simd_chunks .iter() .map(|chunk| IsConsecutive::is_consecutive(*chunk) as usize) .sum(), ); }); }); group.finish();}criterion_group!(benches, simple);criterion_main!(benches);
Если вы хотите запустить этот пример, код находится на GitHub.
Запустите бенчмарк с помощью команды cargo bench
. Отчет появится в target/criterion/simple/report/index.html
и будет включать графики, подобные этому, показывающие, что Splat1 работает гораздо быстрее, чем Regular.
Думаем за пределами Criterion Box
У нас есть проблема. Мы хотим провести бенчмарк sse2
vs. avx2
vs. avx512f
, что требует (в общем случае) нескольких компиляций и запусков criterion
.
Вот наш подход:
- Используйте скрипт Bash для задания переменных среды и вызова бенчмаркинга. Например,
bench.sh
:
#!/bin/bashSIMD_INTEGER_VALUES=("i64" "i32" "i16" "i8" "isize" "u64" "u32" "u16" "u8" "usize")SIMD_LANES_VALUES=(64 32 16 8 4)RUSTFLAGS_VALUES=("-C target-feature=+avx512f" "-C target-feature=+avx2" "")for simdLanes in "${SIMD_LANES_VALUES[@]}"; do for simdInteger in "${SIMD_INTEGER_VALUES[@]}"; do for rustFlags in "${RUSTFLAGS_VALUES[@]}"; do echo "Запуск с SIMD_INTEGER=$simdInteger, SIMD_LANES=$simdLanes, RUSTFLAGS=$rustFlags" SIMD_LANES=$simdLanes SIMD_INTEGER=$simdInteger RUSTFLAGS="$rustFlags" cargo bench done donedone
В дополнение: Вы можете легко использовать Bash в Windows, если у вас есть Git и/или VS Code.
- Используйте
build.rs
, чтобы превратить эти переменные среды в конфигурации Rust:
use std::env;fn main() { if let Ok(simd_lanes) = env::var("SIMD_LANES") { println!("cargo:rustc-cfg=simd_lanes=\"{}\"", simd_lanes); println!("cargo:rerun-if-env-changed=SIMD_LANES"); } if let Ok(simd_integer) = env::var("SIMD_INTEGER") { println!("cargo:rustc-cfg=simd_integer=\"{}\"", simd_integer); println!("cargo:rerun-if-env-changed=SIMD_INTEGER"); }}
- В
benches/build.rs
превратите эти конфигурации в константы и типы Rust:
const SIMD_SUFFIX: &str = if cfg!(target_feature = "avx512f") { "avx512f,512"} else if cfg!(target_feature = "avx2") { "avx2,256"} else if cfg!(target_feature = "sse2") { "sse2,128"} else { "error"};#[cfg(simd_integer = "i8")]type Integer = i8;#[cfg(simd_integer = "i16")]type Integer = i16;#[cfg(simd_integer = "i32")]type Integer = i32;#[cfg(simd_integer = "i64")]type Integer = i64;#[cfg(simd_integer = "isize")]type Integer = isize;#[cfg(simd_integer = "u8")]type Integer = u8;#[cfg(simd_integer = "u16")]type Integer = u16;#[cfg(simd_integer = "u32")]type Integer = u32;#[cfg(simd_integer = "u64")]type Integer = u64;#[cfg(simd_integer = "usize")]type Integer = usize;#[cfg(not(any( simd_integer = "i8", simd_integer = "i16", simd_integer = "i32", simd_integer = "i64", simd_integer = "isize", simd_integer = "u8", simd_integer = "u16", simd_integer = "u32", simd_integer = "u64", simd_integer = "usize")))]type Integer = i32;const LANES: usize = if cfg!(simd_lanes = "2") { 2} else if cfg!(simd_lanes = "4") { 4} else if cfg!(simd_lanes = "8") { 8} else if cfg!(simd_lanes = "16") { 16} else if cfg!(simd_lanes = "32") { 32} else { 64};
- В файле
benches.rs
создайте идентификатор тестирования, записывая комбинацию переменных, которые вы тестируете, разделенные запятыми. Это может быть строка или критерийBenchmarkId
. Я создалBenchmarkId
с помощью следующего вызова:create_benchmark_id::<Integer>("regular", LANES, *parameter)
в эту функцию:
fn create_benchmark_id<T>(name: &str, lanes: usize, parameter: usize) -> BenchmarkIdwhere T: SimdElement,{ BenchmarkId::new( format!( "{},{},{},{},{}", name, SIMD_SUFFIX, type_name::<T>(), mem::size_of::<T>() * 8, lanes, ), parameter, )}
- Для табулирования и анализа мне нравятся результаты бенчмарков в виде значений, разделенных запятыми (CSV). Criterion отошел от файлов
*.csv
к файлам*.json
. Чтобы извлечь*.csv
из*.json
, я создал новую командуcargo
, которую вы можете использовать:criterion-means
.
Установка:
cargo install cargo-criterion-means
Запуск:
cargo criterion-means > results.csv
Пример вывода:
Group,Id,Parameter,Mean(ns),StdErr(ns)vector,regular,avx2,256,i16,16,16,1024,291.47,0.080141vector,regular,avx2,256,i16,16,16,10240,2821.6,3.3949vector,regular,avx2,256,i16,16,16,102400,28224,7.8341vector,regular,avx2,256,i16,16,16,1024000,287220,67.067# ...
Анализ
CSV-файл удобен для анализа с помощью сводных таблиц электронных таблиц или инструментов работы с данными, таких как Polars.
Вот, например, верхняя часть моего 5000-строчного файла Excel:
Столбцы A-J получены из бенчмарка. Столбцы K-N рассчитываются в Excel.
Вот сводная таблица (и график) на основе этих данных. Она показывает влияние изменения количества SIMD-линий на производительность. График усредняет значения для разных типов элементов и длин входных данных. График показывает, что для лучших алгоритмов наилучшие результаты достигаются при 32 или 64 линиях.
С помощью этого анализа мы можем выбрать наш алгоритм и решить, как мы хотим установить параметр LANES.
Вывод
Спасибо, что присоединились ко мне в этом путешествии в мир бенчмарков Criterion.
Если вы еще не использовали Criterion, надеюсь, что это вас побудит попробовать. Если вы использовали Criterion, но не смогли измерить все, что вам важно, надеюсь, что это даст вам путь вперед. Приобретение опыта работы с Criterion в таком расширенном виде позволит получить глубокие понимание характеристик производительности ваших проектов на Rust.
Пожалуйста, подпишитесь на Carl on VoAGI. Я пишу о научном программировании на Rust и Python, машинном обучении и статистике. Обычно я публикую около одной статьи в месяц.