用Criterion评估Rust编译器设置的基准
「使用评估标准对Rust编译器的基准设置进行评估」
使用脚本和环境变量控制标准

本文首先解释了如何使用流行的criterion crate进行基准测试。然后,通过附加信息展示了如何在编译器设置间进行基准测试。尽管每个编译器设置组合需要重新编译和单独运行,我们仍然可以列出和分析结果。这篇文章是一篇伴随着优化 Rust 代码中 SIMD 加速的九个规则的 Towards Data Science 文章的补充。
我们将这个技术应用到了 range-set-blaze
crate 上。我们的目标是测量不同 SIMD(单指令,多数据)设置的性能影响,并比较不同 CPU 上的性能。这种方法也有助于理解不同优化级别的好处。
在 range-set-blaze
的上下文中,我们评估了以下内容:
- 3 个 SIMD 扩展级别 —
sse2
(128 位),avx2
(256 位),avx512f
(512 位) - 10 个元素类型 —
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,isize
,usize
- 5 个 lane 数字 — 4、8、16、32、64
- 2 个 CPU — 搭载
avx512f
的 AMD 7950X,搭载avx2
的 Intel i5–8250U - 5 个算法 — Regular,Splat0,Splat1,Splat2,Rotate
- 4 个输入长度 — 1024;10,240;102,400;1,024,000
其中,我们通过外部调整前四个变量(SIMD 扩展级别、元素类型、lane 数字、CPU)。我们使用常规 Rust 基准测试代码中的循环来控制最后两个变量(算法和输入长度)。
开始使用 Criterion
要在项目中添加基准测试,添加这个 dev 依赖并创建一个子文件夹:
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::*;// 从所使用的 SIMD 扩展创建一个字符串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;// 与此进行比较#[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); // 生成约一百万个对齐元素 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
之外
我们有一个问题。我们想要对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
注意:如果你有Git和/或VS Code,可以轻松在Windows上使用Bash。
- 使用
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
文件。为了从*.json
中提取*.csv
,我创建了一个新的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 lanes数量对吞吐量的影响。图表对元素类型和输入长度进行了平均。图表显示,对于最好的算法,32或64个lanes最好。
通过这种分析,我们现在可以选择我们的算法,决定如何设置LANES参数。
结论
感谢您加入这次深入了解Criterion基准测试的旅程。
如果您以前没有使用过Criterion,我希望这能鼓励您尝试一下。如果您曾经使用过Criterion,但无法测量到您所关心的全部内容,我希望这能为您提供一个前进的方向。以这种扩展方式采用Criterion可以帮助您深入了解Rust项目的性能特性。
请关注Carl的VoAGI。我写关于Rust和Python的科学编程、机器学习和统计方面的文章。我通常每月写一篇文章。