Confronto delle impostazioni del compilatore Rust con Criterion

Confronto delle impostazioni del compilatore Rust con Criterion Scopri quale opzione ottimizza al meglio le tue prestazioni!

Controllo dei criteri con script e variabili d’ambiente

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

Questo articolo spiega, innanzitutto, come effettuare il benchmark utilizzando il popolare framework criterion. Successivamente, fornisce informazioni aggiuntive su come effettuare il benchmark in base alle impostazioni del compilatore. Sebbene ogni combinazione di impostazioni del compilatore richieda una ricompilazione e un’esecuzione separata, è comunque possibile tabulare ed analizzare i risultati. L’articolo è un complemento all’articolo Nine Rules for SIMD Acceleration of Your Rust Code su Towards Data Science.

Abbiamo applicato questa tecnica alla libreria range-set-blaze. Il nostro obiettivo è misurare gli effetti sulle prestazioni di varie impostazioni SIMD (Single Instruction, Multiple Data). Vogliamo anche confrontare le prestazioni tra diverse CPU. Questo approccio è utile anche per capire i vantaggi dei diversi livelli di ottimizzazione.

Nel contesto di range-set-blaze, valutiamo:

  • 3 livelli di estensione SIMD — sse2 (128 bit), avx2 (256 bit), avx512f (512 bit)
  • 10 tipi di elementi — i8, u8, i16, u16, i32, u32, i64, u64, isize, usize
  • 5 numeri di canali — 4, 8, 16, 32, 64
  • 2 CPU — AMD 7950X con avx512f, Intel i5–8250U con avx2
  • 5 algoritmi — Regular, Splat0, Splat1, Splat2, Rotate
  • 4 lunghezze di input — 1024; 10.240; 102.400; 1.024.000

Di queste, regoliamo esternamente le prime quattro variabili (livello di estensione SIMD, tipo di elemento, numero di canali, CPU). Controlliamo le ultime due variabili (algoritmo e lunghezza di input) con cicli all’interno di un normale codice di benchmark Rust.

Primi passi con Criterion

Per aggiungere il benchmark al tuo progetto, aggiungi questa dipendenza di sviluppo e crea una sottocartella:

cargo add criterion --dev --features html_reportsmkdir benches

In Cargo.toml aggiungi:

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

Crea un file benches/bench.rs. Ecco un esempio:

#![feature(portable_simd)]#![feature(array_chunks)]use criterion::{black_box, criterion_group, criterion_main, Criterion};use is_consecutive1::*;// crea una stringa dall'estensione SIMD utilizzataconst 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 {    "errore"};type Integer = i32;const LANES: usize = 64;// confronta con questo#[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}// definisci un benchmark chiamato "simple"fn simple(c: &mut Criterion) {    let mut group = c.benchmark_group("simple");    group.sample_size(1000);    // genera circa 1 milione di elementi allineati    let parameter: Integer = 1_024_000;    let v = (100..parameter + 100).collect::<Vec<_>>();    let (prefix, simd_chunks, reminder) = v.as_simd::<LANES>(); // mantieni la parte allineata    let v = &v[prefix.len()..v.len() - reminder.len()]; // mantieni la parte allineata    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);

Se vuoi eseguire questo esempio, il codice è su GitHub.

Esegui il benchmark con il comando cargo bench. Un rapporto apparirà in target/criterion/simple/report/index.html e includerà grafici come questo che mostrano Splat1 che si esegue molte volte più velocemente di Regular.

Pensiamo oltre la Scatola Criterion

Abbiamo un problema. Vogliamo fare il benchmark di sse2 vs. avx2 vs. avx512f che richiede (generalmente) più compilazioni e esecuzioni di criterion.

Ecco il nostro approccio:

  • Utilizzare uno script Bash per impostare le variabili d’ambiente e chiamare il benchmark. Ad esempio, 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 "Esecuzione con SIMD_INTEGER=$simdInteger, SIMD_LANES=$simdLanes, RUSTFLAGS=$rustFlags"            SIMD_LANES=$simdLanes SIMD_INTEGER=$simdInteger RUSTFLAGS="$rustFlags" cargo bench        done    donedone

Nota: Puoi utilizzare facilmente Bash su Windows se hai Git e/o VS Code.

  • Utilizzare un build.rs per trasformare queste variabili d’ambiente in configurazioni 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");    }}
  • In benches/build.rs trasformare queste configurazioni in costanti e tipi 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};
  • In benches.rs, crea un id di benchmark che registri la combinazione di variabili che stai testando, separate da virgole. Può essere una stringa o un criterio BenchmarkId. Ho creato un BenchmarkId con questa chiamata: create_benchmark_id::<Integer>("regular", LANES, *parameter) a questa funzione:
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,    )}
  • Per la tabulazione e l’analisi, mi piace avere i risultati dei benchmark come valori separati da virgole (CSV). Criterion si è allontanato dai file *.csv e si sta spostando verso i file *.json. Per estrarre i file *.csv dai file *.json, ho creato un nuovo comando cargo che puoi usare: criterion-means.

Installazione:

cargo install cargo-criterion-means

Esegui:

cargo criterion-means > results.csv

Esempio di output:

Gruppo,Id,Parametro,Media(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# ...

Analisi

Un file CSV è adatto per l’analisi tramite tabelle pivot di fogli di calcolo o strumenti per i frame dei dati come Polars.

Ad esempio, ecco l’inizio del mio file dati Excel di 5000 righe:

Le colonne da A a J provengono dal benchmark. Le colonne da K a N sono calcolate da Excel.

Ecco una tabella pivot (e un grafico) basata sui dati. Mostra l’effetto della variazione del numero di corsie SIMD sulla velocità di trasferimento. Il grafico fa la media tra il tipo di elemento e la lunghezza dell’input. Il grafico suggerisce che per gli algoritmi migliori, sia 32 che 64 corsie sono le migliori.

Con questa analisi, possiamo ora scegliere il nostro algoritmo e decidere come vogliamo impostare il parametro LANES.

Conclusioni

Grazie per esserti unito a me in questo viaggio nel benchmarking di Criterion.

Se non hai mai usato Criterion prima, spero che ti incoraggi a provarlo. Se hai usato Criterion ma non sei riuscito a misurare tutto quello che ti interessava, spero che questo ti dia una soluzione. Abbracciare Criterion in questa forma estesa può svelare approfondimenti più profondi sulle caratteristiche di performance dei tuoi progetti Rust.

Per favore, seguite Carl su VoAGI. Scrivo di programmazione scientifica in Rust e Python, di apprendimento automatico e statistica. Di solito scrivo circa un articolo al mese.