用Criterion评估Rust编译器设置的基准

「使用评估标准对Rust编译器的基准设置进行评估」

使用脚本和环境变量控制标准

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

本文首先解释了如何使用流行的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 个元素类型 — i8u8i16u16i32u32i64u64isizeusize
  • 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。我用这个调用创建了一个BenchmarkIdcreate_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的科学编程、机器学习和统计方面的文章。我通常每月写一篇文章。