Benchmarking delle impostazioni del compilatore Rust con Criterion |  di Carl M. Kadie |  Dicembre 2023

 | Intelligenza-Artificiale

Criterio di controllo con script e variabili di ambiente

Cronometrare una corsa di granchi – Fonte: https://openai.com/dall-e-2/. Tutte le altre figure dell’autore.

Questo articolo spiega, in primo luogo, come eseguire il benchmark utilizzando il popolare criterio cassa. Quindi fornisce informazioni aggiuntive che mostrano come eseguire il benchmark tra le impostazioni del compilatore. Sebbene ogni combinazione di impostazioni del compilatore richieda la ricompilazione e un’esecuzione separata, possiamo comunque tabulare e analizzare i risultati. L’articolo è un complemento dell’articolo Nove regole per l’accelerazione SIMD del tuo codice Rust In Verso la scienza dei dati.

Applicheremo questa tecnica al file range-set-blaze cassa. 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 comprendere i vantaggi dei diversi livelli di ottimizzazione.

Nel contesto di range-set-blazevalutiamo:

  • 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 corsia: 4, 8, 16, 32, 64
  • 2 CPU: AMD 7950X con avx512fIntel i5–8250U con avx2
  • 5 algoritmi: Regolare, Splat0, Splat1, Splat2, Ruota
  • 4 lunghezze di ingresso — 1024; 10.240; 102.400; 1.024.000

Di questi, regoliamo esternamente le prime quattro variabili (livello di estensione SIMD, tipo di elemento, numero di corsia, CPU). Abbiamo controllato le ultime due variabili (algoritmo e lunghezza dell’input) con cicli all’interno del normale codice del benchmark Rust.

Per aggiungere benchmarking al tuo progetto, aggiungi questa dipendenza dev e crea una sottocartella:

cargo add criterion --dev --features html_reports
mkdir benches

In Cargo.toml aggiungere:

((bench))
name = "bench"
harness = false

Creare un benches/bench.rs. Ecco un esempio:

#!(feature(portable_simd))
#!(feature(array_chunks))
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use is_consecutive1::*;

// create a string from the SIMD extension used
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"
};
type Integer = i32;
const LANES: usize = 64;
// compare against this
#(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
}
// define a benchmark called "simple"
fn simple(c: &mut Criterion) {
let mut group = c.benchmark_group("simple");
group.sample_size(1000);
// generate about 1 million aligned elements
let parameter: Integer = 1_024_000;
let v = (100..parameter + 100).collect::<Vec<_>>();
let (prefix, simd_chunks, reminder) = v.as_simd::<LANES>(); // keep aligned part
let v = &v(prefix.len()..v.len() - reminder.len()); // keep aligned part
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 file il codice è su GitHub.

Esegui il benchmark con il comando cargo bench. Verrà visualizzato un rapporto target/criterion/simple/report/index.html e include grafici come questo che mostrano che Splat1 funziona molte volte più velocemente di Regular.

Abbiamo un problema. Vogliamo fare dei benchmark sse2 contro avx2 contro avx512f che richiede (generalmente) compilazioni multiple e criterion corre.

Ecco il nostro approccio:

  • Utilizza uno script Bash per impostare variabili di ambiente e chiamare il benchmarking.
    Per esempio, bench.sh:
#!/bin/bash
SIMD_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 "Running with SIMD_INTEGER=$simdInteger, SIMD_LANES=$simdLanes, RUSTFLAGS=$rustFlags"
SIMD_LANES=$simdLanes SIMD_INTEGER=$simdInteger RUSTFLAGS="$rustFlags" cargo bench
done
done
done

A parte: puoi usa facilmente Bash su Windows se hai Git e/o VS Code.

  • Usare 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");
}
}

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.rscrea un ID benchmark che registra la combinazione di variabili che stai testando, separate da virgole. Può essere una stringa o un criterio BenchmarkId. Ho creato un BenchmarkId con questo appello: create_benchmark_id::<Integer>("regular", LANES, *parameter) a questa funzione:
fn create_benchmark_id<T>(name: &str, lanes: usize, parameter: usize) -> BenchmarkId
where
T: SimdElement,
{
BenchmarkId::new(
format!(
"{},{},{},{},{}",
name,
SIMD_SUFFIX,
type_name::<T>(),
mem::size_of::<T>() * 8,
lanes,
),
parameter,
)
}

Installare:

cargo install cargo-criterion-means

Correre:

cargo criterion-means > results.csv

Esempio di uscita:

Group,Id,Parameter,Mean(ns),StdErr(ns)
vector,regular,avx2,256,i16,16,16,1024,291.47,0.080141
vector,regular,avx2,256,i16,16,16,10240,2821.6,3.3949
vector,regular,avx2,256,i16,16,16,102400,28224,7.8341
vector,regular,avx2,256,i16,16,16,1024000,287220,67.067
# ...

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

Ad esempio, ecco la parte superiore del mio file di dati Excel lungo 5000 righe:

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

Ecco una tabella pivot (e un grafico) basata sui dati. Mostra l’effetto della variazione del numero di corsie SIMD sul throughput. Il grafico calcola la media tra il tipo di elemento e la lunghezza dell’input. Il grafico suggerisce che per i migliori algoritmi, la soluzione migliore è 32 o 64 corsie.

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

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

Se non hai mai utilizzato Criterion prima, spero che questo ti incoraggi a provarlo. Se hai utilizzato Criterion ma non sei riuscito a misurare tutto ciò a cui tieni, spero che questo ti dia un percorso da seguire. Abbracciare Criterion in questo modo ampliato può sbloccare informazioni più approfondite sulle caratteristiche prestazionali dei tuoi progetti Rust.

Per favore segui Carl su Medium. Scrivo di programmazione scientifica in Rust e Python, machine learning e statistica. Tendo a scrivere circa un articolo al mese.

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *