Оценка настроек компилятора Rust с помощью Criterion

Оценка производительности компилятора Rust с использованием Criterion

Контроль критерия с помощью скриптов и переменных окружения

Timing a crab race — Source: https://openai.com/dall-e-2/. All other figures from the author.

В этой статье объясняется, во-первых, как провести тестирование с помощью популярного пакета 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.

Начало работы с 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, машинном обучении и статистике. Обычно я публикую около одной статьи в месяц.