クライテリオンを使用したRustコンパイラの設定のベンチマーキング

クライテリオンを使用したRustコンパイラの設定のベンチマーク評価

スクリプトと環境変数を使って基準を制御する

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

まず、この記事では人気のある<criterion crateを使用してのベンチマーク方法を説明します。その後、コンパイラ設定間でのベンチマーク方法についての追加情報を提供します。各コンパイラ設定の組み合わせは再コンパイルと別々の実行が必要ですが、結果を表にまとめて分析することは可能です。この記事はTowards Data Scienceの記事「Nine Rules for SIMD Acceleration of Your Rust Code」の補足となります。</criterion

このテクニックをrange-set-blaze crateに適用します。目的は、さまざまなSIMD(Single Instruction, Multiple Data)設定のパフォーマンス効果を測定することです。また、さまざまなCPU間でのパフォーマンス比較も求めます。このアプローチは、異なる最適化レベルの利点を理解するのにも役立ちます。

range-set-blazeのコンテキストでは、以下を評価します:

  • 3つのSIMD拡張レベル — sse2(128ビット)、avx2(256ビット)、avx512f(512ビット)
  • 10つの要素タイプ — i8、u8、i16、u16、i32、u32、i64、u64、isize、usize
  • 5つのレーン数 — 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

これらのうち、最初の4つの変数(SIMD拡張レベル、要素タイプ、レーン数、CPU)は外部で調整します。最後の2つの変数(アルゴリズムと入力長)は、通常の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);    // 約100万個のアラインされた要素を生成    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が通常のランニングよりもはるかに高速です。

Criterion Boxの外で考える

問題が発生しています。 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内で、テストしている変数の組み合わせをカンマで区切って記録するベンチマークIDを作成します。これは文字列またはBenchmarkIdとして作成できます。私はこの呼び出しでBenchmarkIdを作成しました:create_benchmark_id::("regular", LANES, *parameter) この関数に対して:
fn create_benchmark_id(name: &str, lanes: usize, parameter: usize) -> BenchmarkIdwhere T: SimdElement,{    BenchmarkId::new(        format!(            "{},{},{},{},{}",            name,            SIMD_SUFFIX,            type_name::(),            mem::size_of::() * 8,            lanes,        ),        parameter,    )}
  • 表形式や分析のために、ベンチマークの結果をカンマ区切りの値(CSV)形式で表示することが好きです。Criterionは *.csv ファイルから *.json ファイルへと移行しました。 *.json から *.csv を抽出するために、criterion-means という新しい cargo コマンドを作成しました。

インストール:

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

分析

スプレッドシートのピボットテーブルPolarsなどのデータフレームツールを使用して、CSVファイルを分析することができます。

例えば、5000行のExcelデータファイルの先頭部分は以下のようになります:

AからJ列はベンチマークから取得されたデータで、KからN列はExcelによって計算されたものです。

以下はデータを基にしたピボットテーブル(およびグラフ)の例です。これはSIMDレーンの数を変化させたときのスループットの影響を示しています。チャートは要素のタイプと入力長にわたって平均化されています。最良のアルゴリズムでは、32レーンまたは64レーンが最適であることを示唆しています。

この分析により、アルゴリズムを選択し、LANESパラメータを設定する方法を決定することができます。

結論

Criterionベンチマークへの参加ありがとうございました。

Criterionを使用したことがない場合、この記事が試してみるきっかけになることを願っています。Criterionを使用してもうまく測定できなかった場合、これが前進するための道筋となることを願っています。拡張された方法でCriterionを取り入れることによって、Rustプロジェクトのパフォーマンス特性に関するより深い洞察を得ることができます。

VoAGIでCarlをフォローしてください。私はRustとPythonでの科学的プログラミング、機械学習、統計について執筆することが多いです。1ヶ月に1つの記事を書く傾向があります。

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more