Evaluación comparativa de la configuración del compilador Rust con Criterion

Control de Criterion con scripts y variables de entorno

Cronometrar una carrera de cangrejos – Fuente: https://openai.com/dall-e-2/. Todas las demás figuras del autor.

Este artículo explica, primero, cómo realizar evaluaciones comparativas utilizando el popular criterio caja. Luego, brinda información adicional que muestra cómo realizar comparaciones entre las configuraciones del compilador. Aunque cada combinación de configuraciones del compilador requiere una nueva compilación y una ejecución por separado, aún podemos tabular y analizar los resultados. El artículo es un complemento del artículo. Nueve reglas para la aceleración SIMD de su código Rust en Hacia la ciencia de datos.

Aplicaremos esta técnica a la alcance-prendido-incendio caja. Nuestro objetivo es medir los efectos en el rendimiento de varias configuraciones SIMD (instrucción única, datos múltiples). También queremos comparar el rendimiento entre diferentes CPU. Este enfoque también es útil para comprender los beneficios de los diferentes niveles de optimización.

En el contexto del incendio, evaluamos:

  • 3 niveles de extensión SIMD: sse2 (128 bits), avx2 (256 bits), avx512f (512 bits)
  • 10 tipos de elementos: i8, u8, i16, u16, i32, u32, i64, u64, isize, usize
  • 5 números de carril: 4, 8, 16, 32, 64
  • 2 CPU: AMD 7950X con avx512f, Intel i5–8250U con avx2
  • 5 algoritmos: Regular, Splat0, Splat1, Splat2, Rotar
  • 4 longitudes de entrada: 1024; 10.240; 102.400; 1.024.000

De estos, ajustamos externamente las primeras cuatro variables (nivel de extensión SIMD, tipo de elemento, número de carril, CPU). Controlamos las dos variables finales (algoritmo y longitud de entrada) con bucles dentro del código de referencia normal de Rust.

Primeros pasos con el criterio

Para agregar evaluaciones comparativas a su proyecto, agregue esta dependencia de desarrollo y cree una subcarpeta:

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

En Cargo.toml agregue:

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

Crea bancos/bench.rs. Aquí hay una muestra:

#![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);

Si desea ejecutar este ejemplo, el el código está en GitHub.

Ejecute el punto de referencia con el banco de carga de comando. Aparecerá un informe en target/criterion/simple/report/index.html e incluye gráficos como este que muestran a Splat1 ejecutándose muchas veces más rápido que Regular.

Pensar fuera del cuadro de criterios

Tenemos un problema. Queremos comparar sse2 con avx2 y avx512f, lo que requiere (generalmente) múltiples compilaciones y ejecuciones de criterios.

Este es nuestro enfoque:

  • Utilice un script Bash para establecer variables de entorno y realizar evaluaciones comparativas.
    Por ejemplo, banco.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

Aparte: puedes use fácilmente Bash en Windows si tienes Git y/o VS Code.

  • Usar una construir.rs para convertir estas variables de entorno en configuraciones de 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");
}
}
  • En bancos/build.rs Convierta estas configuraciones en constantes y tipos de 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
};
  • En benches.rs, cree una identificación de referencia que registre la combinación de variables que está probando, separadas por comas. Puede ser una cadena o un criterio BenchmarkId. Creé un BenchmarkId con esta llamada: create_benchmark_id::("regular"LANES, *parámetro) a esta función:
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,
)
}

Instalar:

cargo install cargo-criterion-means

Correr:

cargo criterion-means > results.csv

Ejemplo de salida:

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
# ...

Análisis

Un archivo CSV es adecuado para el análisis mediante tablas dinámicas de hojas de cálculo o herramientas de marco de datos como Polares.

Por ejemplo, aquí está la parte superior de mi archivo de datos de Excel de 5000 líneas:

Las columnas A a J provienen del punto de referencia. Las columnas K a N se calculan en Excel.

Aquí hay una tabla dinámica (y un gráfico) basada en los datos. Muestra el efecto de variar el número de carriles SIMD en el rendimiento. El gráfico promedia el tipo de elemento y la longitud de entrada. El gráfico sugiere que para los mejores algoritmos, lo mejor es 32 o 64 carriles.

Con este análisis, ahora podemos elegir nuestro algoritmo y decidir cómo queremos configurar el parámetro LANES.

Conclusión

Gracias por acompañarme en este viaje hacia la evaluación comparativa de Criterion.

Si no has usado Criterion antes, espero que esto te anime a probarlo. Si ha utilizado Criterion pero no pudo lograr que mida todo lo que le interesaba, espero que esto le indique un camino a seguir. Adoptar Criterion de esta manera ampliada puede desbloquear conocimientos más profundos sobre las características de rendimiento de sus proyectos de Rust.

Por favor sigue a Carl en Medium. Escribo sobre programación científica en Rust y Python, aprendizaje automático y estadística. Tiendo a escribir alrededor de un artículo por mes.


Evaluación comparativa de la configuración del compilador Rust con Criterion fue publicado originalmente en Hacia la ciencia de datos en Medium, donde las personas continúan la conversación resaltando y respondiendo a esta historia.