「RustコードのSIMDアクセラレーションのための9つのルール(パート1)」

「RustコードにおけるSIMDアクセラレーションのための9つのルール(パート1)」

レンジセットブレイズのデータインジェクションを7倍に増やす際の一般的な教訓

カニが小さいカニに計算を委任している—出典:https://openai.com/dall-e-2/。その他の図は作者のもの。

Seattle Rust MeetupのBen Lichtman (B3NNY)にSIMDに関する正しい方向性を示してもらいました。

SIMD(シングルインストラクション、マルチプルデータ)演算は、2000年代初頭以来、Intel/AMDとARMのCPUの特徴となっています。これらの演算は、例えば、8つのi32配列に対して他の8つのi32配列を1つのCPU演算で1つのコア上で加算することができます。SIMD演算を使用することで、特定のタスクを大幅に高速化することができます。SIMDを使用していない場合、CPUの能力を十分に活用していない可能性があります。

これは「またしてもRustとSIMDの記事?」と思われるかもしれません。はい、SIMDをプログラムの問題に適用し、それについての記事を書く必要を感じました。ただし、この記事では、プロジェクトを進めるための深い理解も提供できるようになっています。Rustのnightlyで利用可能となったSIMDの機能と設定について説明します。Rust SIMDのチートシートも含まれています。また、安全なRustを離れることなく、SIMDコードをジェネリック化する方法も示されています。GodboltやCriterionなどのツールの使用方法も紹介します。最後に、プロセスを容易にする新しいcargoコマンドも紹介します。

range-set-blazeクレートでは、RangeSetBlaze::from_iterメソッドを使用して、長い整数のシーケンスを取り込んでいます。整数が「重なり合っている」場合、これはRustの標準的なHashSet::from_iterよりも30倍速くなることがあります。SIMD演算を使用すれば、さらに改善できるのでしょうか?はい!

「重なり合っている」の定義については、以前の記事のルール2を参照してください。また、整数が重なり合っていない場合はどうなるのか? RangeSetBlazeHashSetよりも2〜3倍遅いです。

重なり合う整数に対して、SIMD演算に基づく新しいRangeSetBlaze::from_sliceメソッドは、RangeSetBlaze::from_iterよりも7倍速くなります。これにより、HashSet::from_iterよりも200倍以上速くなります。(整数が重なり合わない場合は、HashSetよりも2〜3倍遅いです。)

この高速化の過程で、SIMD演算を使用してプロジェクトを加速するための9つのルールを学びました。

これらのルールは以下の通りです:

  1. 夜間のRustとcore::simdを使用する – Rustの実験的な標準SIMDモジュール。
  2. CCC:コンピュータのSIMD能力をチェックし、制御し、選択する。
  3. core::simdを選択的に学ぶ。
  4. 候補のアルゴリズムを考え出す。
  5. GodboltとAIを使用してコードのアセンブリを理解する(アセンブリ言語を知らなくても)。
  6. インラインのジェネリックスによるすべてのタイプとLANESの一般化(うまくいかない場合は)マクロ、(うまくいかない場合は)トレイトを使用する。

これらのルールの詳細については、第2部を参照してください:

7. アルゴリズムを選択するためのCriterionベンチマーキングおよびLANESは(ほとんど)常に32または64であることを発見するために使用する。

8. as_simdi128/u128用の特別なコード、および追加のコンテキスト内のベンチマーキングを使用して、最良のSIMDアルゴリズムをプロジェクトに統合します。

9. 最良のSIMDアルゴリズムを(一旦)プロジェクトから取り外します。オプションのCargoの機能を使って取り外すこともできます。

補足:はっきりさせるために、「ルール」と呼んでいますが、もちろんこれらは単なる提案です。

ルール1:Nightly Rustとcore::simd、Rustの実験的な標準SIMDモジュールを使用します。

大規模なプロジェクトでSIMD演算を使用する前に、まずそれらが動作することを確認しましょう。以下は手順です:

まず、simd_helloというプロジェクトを作成します:

cargo new simd_hellocd simd_hello

src/main.rsを編集して以下のコードを挿入します(Rust playground):

// Nightly Rustに 'portable_simd' を有効化するよう指示します
#![feature(portable_simd)]use core::simd::prelude::*;// 定数のSimd構造体const LANES: usize = 32;const THIRTEENS: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([13; LANES]);const TWENTYSIXS: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([26; LANES]);const ZEES: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([b'Z'; LANES]);fn main() {    // LANESバイトのスライスからSimd構造体を作成    let mut data = Simd::<u8, LANES>::from_slice(b"URYYBJBEYQVQBUBCRVGFNYYTBVATJRYY");    data += THIRTEENS; // 各バイトに13を加算    // 各バイトを 'Z' と比較し、バイトが 'Z' より大きい場合には26を減算    let mask = data.simd_gt(ZEES); // 各バイトを 'Z' と比較    data = mask.select(data - TWENTYSIXS, data);    let output = String::from_utf8_lossy(data.as_array());    assert_eq!(output, "HELLOWORLDIDOHOPEITSALLGOINGWELL");    println!("{}", output);}

次に、全体のSIMD機能を使用するにはNightly版のRustが必要です。Rustがインストールされていることを前提に、Nightly版をインストールしてください (rustup install nightly)。最新のNightly版を使用していることを確認してください (rustup update nightly)。最後に、このプロジェクトでNightly版を使用するように設定します (rustup override set nightly)。

これで cargo run でプログラムを実行できます。このプログラムは32バイトの大文字の文字列に ROT13復号 を適用します。SIMDを使用すると、プログラムは32バイトを同時に復号化できます。

プログラムの各セクションを見て、どのように動作するか見てみましょう。それは次のように始まります:

#![feature(portable_simd)]use core::simd::prelude::*;

Rust Nightlyは、追加の機能を要求された場合にのみその追加機能を提供します。 #![feature(portable_simd)] は、Rust Nightlyに新しい実験的な core::simd モジュールを利用可能にするよう要求するステートメントです。その後の use ステートメントは、モジュールの最も重要な型とトレイトをインポートします。

コードの次のセクションでは、有用な定数を定義しています:

const LANES: usize = 32;const THIRTEENS: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([13; LANES]);const TWENTYSIXS: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([26; LANES]);const ZEES: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([b'Z'; LANES]);

Simd構造体は、特別な種類のRust配列です。 (たとえば、常にメモリアラインドされています。)定数LANESは、Simd配列の長さを示します。 from_arrayコンストラクタは、通常のRust配列をコピーしてSimdを作成します。 この場合、const Simdが必要なので、構築する配列もconstでなければなりません。

次の2行は、暗号化されたテキストをdataにコピーし、それぞれの文字に13を加えます。

let mut data = Simd::<u8, LANES>::from_slice(b"URYYBJBEYQVQBUBCRVGFNYYTBVATJRYY");data += THIRTEENS;

もし間違えて暗号化されたテキストが正確にLANES(32)の長さでない場合はどうなるでしょうか? 残念ながら、コンパイラは教えてくれません。 その代わり、プログラムを実行すると、from_sliceがパニックします。 暗号化されたテキストに大文字以外の文字が含まれている場合はどうなるでしょうか? この例では、その可能性を無視します。

+=オペレータは、Simd dataSimd THIRTEENSの要素ごとの足し算を行います。 結果はdataに入ります。 通常のRustのデバッグビルドでは、オーバーフローのチェックが行われますが、SIMDではそうではありません。 RustはSIMD算術演算子を常にラップするように定義しています。 u8型の値は255を超えたらラップします。

偶然なことに、Rot13の復号もwrappingが必要ですが、255ではなく「Z」を超えた後に必要です。 必要なRot13のwrappingを行うためのアプローチを以下に示します。 値が「Z」を超える要素ごとに26を引きます。

let mask = data.simd_gt(ZEES);data = mask.select(data - TWENTYSIXS, data);

これは要素ごとに「Z」を超える場所を見つけます。 そして、すべての値から26を引きます。 興味のある場所では、引かれた値を使用します。 他の場所では、元の値を使用します。 すべての値から引いてから、一部の値だけを使用することは無駄でしょうか? SIMDでは、これには余分なコンピュータ時間がかかりませんし、ジャンプも回避できます。 したがって、この戦略は効率的で一般的です。

プログラムは次のように終了します:

let output = String::from_utf8_lossy(data.as_array());assert_eq!(output, "HELLOWORLDIDOHOPEITSALLGOINGWELL");println!("{}", output);

.as_array()メソッドに注意してください。 これはSimd構造体を通常のRust配列に安全に変換しますが、コピーはしません。

私にとって驚きでしたが、このプログラムはSIMD拡張機能を持たないコンピュータでも正常に実行されます。 Rust Nightlyはコードを通常の(非SIMD)命令にコンパイルします。 ただし、「良い」と実行したいだけでなく、もっと速く実行したいと思います。 それには、コンピュータのSIMDパワーを活用する必要があります。

ルール2:CCC:コンピュータのSIMD機能をチェック、制御、選択する。

マシン上でSIMDプログラムをより速く実行するためには、まずマシンがどのSIMD拡張をサポートしているかを見つける必要があります。 Intel/AMDマシンをお持ちの場合、私のsimd-detectカーゴコマンドを使用できます。

次のように実行します:

rustup override set nightlycargo install cargo-simd-detect --forcecargo simd-detect

私のマシンでは、次の出力が表示されます:

extension       width                   available       enabledsse2            128-bit/16-bytes        true            trueavx2            256-bit/32-bytes        true            falseavx512f         512-bit/64-bytes        true            false

これは、私のマシンがsse2avx2、およびavx512fのSIMD拡張をサポートしていることを示しています。 そのうち、デフォルトではRustは普及している20年前のsse2拡張を有効にしています。

SIMD拡張機能は、avx512favx2より上で、sse2より上です。上位の拡張機能を有効にすると、下位の拡張機能も有効になります。

ほとんどのIntel/AMDコンピュータは、10年前のavx2拡張機能もサポートしています。環境変数を設定することで有効にすることができます:

# Windowsコマンドプロンプトの場合set RUSTFLAGS=-C target-feature=+avx2# Unix系のシェル(Bashなど)の場合export RUSTFLAGS="-C target-feature=+avx2"

“force install”を実行し、再びsimd-detectを実行すれば、avx2が有効になっているはずです。

# 'enabled'の変更を確認するために毎回force installを実行cargo install cargo-simd-detect --forcecargo simd-detect

extension         width                   available       enabledsse2            128ビット/16バイト        true            trueavx2            256ビット/32バイト        true            trueavx512f         512ビット/64バイト        true            false

または、マシンがサポートするすべてのSIMD拡張機能をオンにすることもできます:

# Windowsコマンドプロンプトの場合set RUSTFLAGS=-C target-cpu=native# Unix系のシェル(Bashなど)の場合export RUSTFLAGS="-C target-cpu=native"

私のマシンでは、これによりavx512fが有効になります。これは、一部のIntelコンピュータと一部のAMDコンピュータでサポートされている新しいSIMD拡張機能です。

マシンのSIMD拡張機能をデフォルトに戻すには、次のように設定します(Intel/AMDの場合はsse2です):

# Windowsコマンドプロンプトの場合set RUSTFLAGS=# Unix系のシェル(Bashなど)の場合unset RUSTFLAGS

なぜtarget-cpu=nativeがRustのデフォルトではないか疑問に思うかもしれません。問題は、avx2avx512fを使用して作成したバイナリは、それらのSIMD拡張機能がないコンピュータで実行できないということです。したがって、自分自身の使用のためにコンパイルしている場合は、target-cpu=nativeを使用します。ただし、他の人のためにコンパイルしている場合は、注意してSIMD拡張機能を選択し、どのSIMD拡張機能レベルを前提としているかを知らせてください。

幸いなことに、選択したSIMD拡張機能のレベルに関係なく、RustのSIMDサポートは非常に柔軟であり、後で簡単に決定を変更することができます。次に、RustでSIMDを使用したプログラミングの詳細を学びましょう。

ルール3: core::simdを選択的に学ぶ。

Rustの新しいcore::simdモジュールをビルドするためには、選択したビルディングブロックを学ぶ必要があります。以下は、私が最も有用と考える構造体、メソッドなどのチートシートです。各項目には、それに対するドキュメントへのリンクが含まれています。

構造体

  • Simd – 特別な、アラインされた、固定長のSimdElementの配列。配列内の位置とその位置に格納されている要素を「レーン」と呼びます。デフォルトでは、Simdの構造体は参照ではなくコピーされます。
  • Mask – レーンごとの含まれる/含まれないを示す特別なブール配列。

SimdElements

  • 浮動小数点型: f32f64
  • 整数型: i8u8i16u16i32u32i64u64isizeusize
  • ただしi128, u128ではありません。

Simd コンストラクタ

  • Simd::from_array – 固定長配列をコピーしてSimd構造体を作成します。
  • Simd::from_slice – スライスの最初のLANE要素をコピーしてSimd<T,LANE>構造体を作成します。
  • Simd::splatSimd構造体のすべてのレーンに単一の値を複製します。
  • slice::as_simd – コピーせずに、通常のスライスをSimdのアラインされたスライスに安全に変換します(非アラインドの残りも含む)。

Simd 変換

  • Simd::as_array – コピーせずに、Simd構造体を通常の配列参照に安全に変換します。

Simd メソッドと演算子

  • simd[i]Simdのレーンから値を抽出します。
  • simd + simd – 2つのSimd構造体の要素ごとの加算を行います。また、-*/%、剰余、ビットごとのAND、OR、XOR、NOT、シフトもサポートされています。
  • simd += simd – 別のSimd構造体を現在の構造体にインプレースで追加します。他の演算子もサポートされています。
  • Simd::simd_gt – 2つのSimd構造体を比較し、最初の要素が2番目の要素よりも大きいかどうかを示すMaskを返します。また、simd_ltsimd_lesimd_gesimd_ltsimd_eqsimd_neもサポートされています。
  • Simd::rotate_elements_left – 指定された回数だけSimd構造体の要素を左に回転させます。また、rotate_elements_rightもあります。
  • simd_swizzle!(simd, indexes) – 指定された定数インデックスに基づいて、Simd構造体の要素を並べ替えます。
  • simd == simd – 2つのSimd構造体の等価性をチェックし、通常のbool結果を返します。
  • Simd::reduce_andSimd構造体のすべてのレーンでビットごとのAND演算を行います。また、reduce_orreduce_xorreduce_maxreduce_minreduce_sum(ただしreduce_eqはなし)もサポートされています。

Mask メソッドと演算子

  • Mask::select – マスクに基づいて2つのSimd構造体から要素を選択します。
  • Mask::all – マスクがすべてtrueかどうかを判定します。
  • Mask::any – マスクにtrueが含まれるかどうかを判定します。

レーンについてのすべて

  • Simd::LANESSimd構造体の要素(レーン)の数を示す定数です。
  • SupportedLaneCountLANESの許可される値を示します。ジェネリクスで使用します。
  • simd.lanesSimd構造体のレーン数を示すconstメソッドです。

低レベルのアライメント、オフセットなど

可能な場合は、to_simdを使用してください。

  • mem::size_ofmem::align_ofmem::align_tointrinsics::offsetpointer::read_unaligned(unsafe)、pointer::write_unaligned(unsafe)、mem::transmute(unsafe、const)

それ以外にも興味があるかもしれないもの

  • deinterleavegather_orreversescatter

これらのビルディングブロックが手元に揃っているので、何かを構築する時が来ました。

ルール4:候補アルゴリズムを考え出す。

何を高速化したいですか?最初からどのSIMDアプローチが最適かはわかりません。したがって、多くのアルゴリズムを作成し、それを分析(ルール5)およびベンチマーク(ルール7)する必要があります。

私は「range-set-blaze」という、”塊状”の整数セットを操作するためのクレートを高速化したかったです。私は、「is_consecutive」という、連続した整数のブロックを検出する関数が有用であることを期待していました。

背景: Crate range-set-blazeは、”塊状”の整数に作用します。ここでいう”塊状”は、データを表現するのに必要な範囲の数が、入力整数の数に比べて少ないことを意味します。例えば、以下の1002個の入力整数

100, 101, …, 489, 499, 501, 502,, …, 998, 999, 999, 100, 0

は、最終的には3つのRustの範囲になります:

0..=0, 100..=499, 501..=999

(内部的には、RangeSetBlaze構造体は、BTreeMapに格納された、キャッシュ効率の良い、互いに素の範囲のソート済みリストとして整数のセットを表します。)

入力整数は非ソートかつ冗長な場合も許可されていますが、通常は「望ましい」形になることを期待しています。RangeSetBlazeのfrom_iterコンストラクタは、この期待に基づいて隣接する整数をグループ化します。例えば、from_iterではまず、1002個の入力整数を以下の4つの範囲に変換します:

100..=499, 501..=999, 100..=100, 0..=0.

これにより、入力サイズに関係なく、最小限の一定のメモリ使用量でソートされ、マージされます。

新しいfrom_sliceメソッドが、配列のような入力からの構築を高速化するために、いくつかの連続した整数を素早く見つけることができるかどうかを考えてみました。例えば、最小限の一定のメモリで、1002個の入力整数を以下の5つのRustの範囲に変換できるでしょうか:

100..=499, 501..=999, 999..=999, 100..=100, 0..=0.

もしそうなら、from_iterは処理を素早く終了することができるでしょう。

まずは通常のRustでis_consecutiveを記述しましょう:

pub const LANES: usize = 16;pub fn is_consecutive_regular(chunk: &[u32; LANES]) -> bool {    for i in 1..LANES {        if chunk[i - 1].checked_add(1) != Some(chunk[i]) {            return false;        }    }    true}

このアルゴリズムは、配列を順次ループし、各値が1つ前の値よりも1だけ大きいかどうかをチェックします。また、オーバーフローを回避します。

アイテムをループしているだけなので、SIMDがどれだけ改善できるかわかりませんでした。最初の試みは次のようなものでした:

Splat0

use std::simd::prelude::*;const COMPARISON_VALUE_SPLAT0: Simd<u32, LANES> =    Simd::from_array([15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);pub fn is_consecutive_splat0(chunk: Simd<u32, LANES>) -> bool {    if chunk[0].overflowing_add(LANES as u32 - 1) != (chunk[LANES - 1], false) {        return false;    }    let added = chunk + COMPARISON_VALUE_SPLAT0;    Simd::splat(added[0]) == added}

以下はその計算の概要です:

ソース:この画像および以下の画像すべて、著者によるもの。

最初に、最初のアイテムと最後のアイテムが15個離れているかどうか(不要に)チェックします。その後、0番目のアイテムに15を追加し、次のアイテムには14を追加することで、addedを作成します。最後に、addedのすべてのアイテムが同じかどうかを確認するために、addedの0番目のアイテムを基に新しいSimdを作成し、比較します。なお、splatは1つの値からSimd構造体を作成します。

Splat1&Splat2

is_consecutiveの問題をBen Lichtmanに言及したところ、彼は独自にSplat1を考案しました:

const COMPARISON_VALUE_SPLAT1: Simd<u32, LANES> =    Simd::from_array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);pub fn is_consecutive_splat1(chunk: Simd<u32, LANES>) -> bool {    let subtracted = chunk - COMPARISON_VALUE_SPLAT1;    Simd::splat(chunk[0]) == subtracted}

Splat1は、chunkから比較値を引き、結果がchunkの最初の要素と同じかどうかをチェックします。

彼はまた、Splat2というバリエーションも考案しました。この場合、最初の要素の代わりにsubtractedをスプラットします。これにより、1回のメモリアクセスが削減されると思われます。

これらの中で最も優れているのはどれかと思いますが、それについて議論する前に、さらに2つの候補を見てみましょう。

Swizzle

SwizzleはSplat2に似ていますが、splatの代わりにsimd_swizzle!を使用します。マクロsimd_swizzle!は、Simdの古いオブジェクトのレーンをインデックスの配列に従って並べ替え、新しいSimdを作成します。

pub fn is_consecutive_sizzle(chunk: Simd<u32, LANES>) -> bool {    let subtracted = chunk - COMPARISON_VALUE_SPLAT1;    simd_swizzle!(subtracted, [0; LANES]) == subtracted}

Rotate

これは異なります。私はこれに高い期待を持っていました。

const COMPARISON_VALUE_ROTATE: Simd<u32, LANES> =    Simd::from_array([4294967281, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);pub fn is_consecutive_rotate(chunk: Simd<u32, LANES>) -> bool {    let rotated = chunk.rotate_elements_right::<1>();    chunk - rotated == COMPARISON_VALUE_ROTATE}

アイデアは、すべての要素を右に1つ回転することです。その後、元のchunkからrotatedを引きます。入力が連続している場合、結果は「-15」に続いてすべて1になるはずです(ラップされた減算を使用して、-15は4294967281u32です)。

今、候補者がいるので、評価を始めましょう。

ルール5: アセンブリ言語が分からなくても、GodboltとAIを使用してコードのアセンブリを理解する。

候補者の評価は2つの方法で行います。最初に、このルールでは、コードから生成されるアセンブリ言語を見てみましょう。次に、Rule 7では、コードの速度をベンチマークします。

アセンブリ言語が分からなくても心配しないでください。それを見ることで何か得ることができます。

生成されたアセンブリ言語を見る最も簡単な方法は、Compiler Explorer、またはGodboltを使用することです。外部のクレートを使用しない短いコードに最適です。以下のようになります:

上の図の数字に従って、Godboltを使用するための手順を踏んでください:

  1. ウェブブラウザでgodbolt.orgを開きます。
  2. 新しいソースエディタを追加します。
  3. 言語としてRustを選択します。
  4. 興味のあるコードを貼り付けます。関数はpublicにしましょう(pub fn)。main関数や不要な関数は含めないでください。ツールは外部のクレートをサポートしません。
  5. 新しいコンパイラを追加します。
  6. コンパイラのバージョンをnightlyに設定します。
  7. オプションを設定します(現時点では):-C opt-level=3 -C target-feature=+avx512f
  8. エラーがあれば、出力を確認します。
  9. ツールの状態を共有したり保存したりする場合は、「共有」をクリックします

上の画像からわかるように、Splat2とSizzleはまったく同じですので、Sizzleは考慮から外すことができます。もし私のGodboltセッションを開くと、ほとんどの関数がほぼ同じアセンブリ命令にコンパイルされることもわかるでしょう。例外としては、Regular(はるかに長い)とSplat0(初期チェックを含む)があります。

アセンブリで、512ビットのレジスタはZMMから始まります。256ビットのレジスタはYMMから始まります。128ビットのレジスタはXMMから始まります。生成されたアセンブリをよりよく理解するためには、AIツールを使用して注釈を生成します。たとえば、ここではSplat2についてBing Chatに質問しています:

-C target-feature=+avx2target-featureを完全に省略するなど、異なるコンパイラ設定を試してみてください。

アセンブリ命令が少なくても高速な速度が得られるわけではありません。しかし、アセンブリを見ることで、コンパイラがSIMD命令やコンスタント参照のインライン展開などを努力していることを確認できます。また、Splat1とSwizzleのように、2つの候補が同じであることを知ることもあります。

Godboltが提供するディスアセンブリ機能以上の機能が必要な場合、外部のクレートを使用するコードとの作業ができる能力などが必要となります。私にはargo-show-asmというcargoツールがおすすめされました。使ってみて、比較的簡単に使えるとわかりました。

range-set-blazeクレートは、u32を超える整数型に対応する必要があります。さらに、16 LANESが常に最適とは限らない理由があります。これらのニーズに対応するために、次のルールではコードを一般化します。

ルール6: インラインのジェネリックを使用して、すべてのタイプとLANESを一般化する(それが機能しない場合は)マクロ、そして(それが機能しない場合は)トレイト。

まず、ジェネリックを使用してSplat1を一般化しましょう。

#[inline]pub fn is_consecutive_splat1_gen<T, const N: usize>(    chunk: Simd<T, N>,    comparison_value: Simd<T, N>,) -> boolwhere    T: SimdElement + PartialEq,    Simd<T, N>: Sub<Simd<T, N>, Output = Simd<T, N>>,    LaneCount<N>: SupportedLaneCount,{    let subtracted = chunk - comparison_value;    Simd::splat(chunk[0]) == subtracted}

まず、#[inline]属性に注意してください。効率のために重要であり、ほとんどの小さな関数に使用します。

上記で定義された関数is_consecutive_splat1_genは素晴らしいですが、定義していない2番目の入力であるcomparison_valueが必要です。

ジェネリックなconst comparison_valueが必要ない場合、羨ましいです。次のルールに進むこともできます。同様に、家事をするために個人用ロボットを持つのと同じくらい簡単にジェネリックなconst comparison_valueを作成できる場合は、二重に羨ましいです。

ジェネリックで定数のcomparison_value_splat_genを作成しようとしてみることができます。残念ながら、From<usize>も代替T::Oneも定数ではないため、これは機能しません:

// DOESN'T WORK BECAUSE From<usize> is not constpub const fn comparison_value_splat_gen<T, const N: usize>() -> Simd<T, N>where    T: SimdElement + Default + From<usize> + AddAssign,    LaneCount<N>: SupportedLaneCount,{    let mut arr: [T; N] = [T::from(0usize); N];    let mut i_usize = 0;    while i_usize < N {        arr[i_usize] = T::from(i_usize);        i_usize += 1;    }    Simd::from_array(arr)}

マクロは極悪人の最後の避難所です。 したがって、マクロを使用しましょう:

#[macro_export]macro_rules! define_is_consecutive_splat1 {    ($function:ident, $type:ty) => {        #[inline]        pub fn $function<const N: usize>(chunk: Simd<$type, N>) -> bool        where            LaneCount<N>: SupportedLaneCount,        {            define_comparison_value_splat!(comparison_value_splat, $type);            let subtracted = chunk - comparison_value_splat();            Simd::splat(chunk[0]) == subtracted        }    };}#[macro_export]macro_rules! define_comparison_value_splat {    ($function:ident, $type:ty) => {        pub const fn $function<const N: usize>() -> Simd<$type, N>        where            LaneCount<N>: SupportedLaneCount,        {            let mut arr: [$type; N] = [0; N];            let mut i = 0;            while i < N {                arr[i] = i as $type;                i += 1;            }            Simd::from_array(arr)        }    };}

これにより、任意の要素タイプおよびLANESの数で実行できます(Rustプレイグラウンド):

define_is_consecutive_splat1!(is_consecutive_splat1_i32, i32);let a: Simd<i32, 16> = black_box(Simd::from_array(array::from_fn(|i| 100 + i as i32)));let ninety_nines: Simd<i32, 16> = black_box(Simd::from_array([99; 16]));assert!(is_consecutive_splat1_i32(a));assert!(!is_consecutive_splat1_i32(ninety_nines));

残念ながら、これではrange-set-blazeには十分ではありません。これはすべての要素タイプ(ただし1つだけではなく)および(理想的には)すべてのLANE(ただ1つだけではなく)で実行する必要があります。

幸いなことに、マクロに依存する回避策があります。これは再びマクロに依存します。また、i8i16i32i64isizeu8u16u32u64、およびusizeの有限リストのタイプのみをサポートする必要があります。 f32およびf64もサポートする必要がある場合(または代わりに)、そのようにしてください。

一方、i128およびu128をサポートする必要がある場合、何もすることができません。 core::simdモジュールはそれらをサポートしていません。パフォーマンスのコストでrange-set-blazeがそれを回避する方法については、後でルール8で説明します。

回避策では、IsConsecutiveという新しいトレイトを定義します。次に、マクロを使用して興味のある10のタイプに対してトレイトを実装します。

pub trait IsConsecutive {    fn is_consecutive<const N: usize>(chunk: Simd<Self, N>) -> bool    where        Self: SimdElement,        Simd<Self, N>: Sub<Simd<Self, N>, Output = Simd<Self, N>>,        LaneCount<N>: SupportedLaneCount;}macro_rules! impl_is_consecutive {    ($type:ty) => {        impl IsConsecutive for $type {            #[inline] // 非常に重要            fn is_consecutive<const N: usize>(chunk: Simd<Self, N>) -> bool            where                Self: SimdElement,                Simd<Self, N>: Sub<Simd<Self, N>, Output = Simd<Self, N>>,                LaneCount<N>: SupportedLaneCount,            {                define_is_consecutive_splat1!(is_consecutive_splat1, $type);                is_consecutive_splat1(chunk)            }        }    };}impl_is_consecutive!(i8);impl_is_consecutive!(i16);impl_is_consecutive!(i32);impl_is_consecutive!(i64);impl_is_consecutive!(isize);impl_is_consecutive!(u8);impl_is_consecutive!(u16);impl_is_consecutive!(u32);impl_is_consecutive!(u64);impl_is_consecutive!(usize);

これで完全に一般的なコード(Rust Playground)を呼び出すことができます:

// i32と16LANEで機能するlet a: Simd<i32, 16> = black_box(Simd::from_array(array::from_fn(|i| 100 + i as i32)));let ninety_nines: Simd<i32, 16> = black_box(Simd::from_array([99; 16]));assert!(IsConsecutive::is_consecutive(a));assert!(!IsConsecutive::is_consecutive(ninety_nines));// i8と64LANEで機能するlet a: Simd<i8, 64> = black_box(Simd::from_array(array::from_fn(|i| 10 + i as i8)));let ninety_nines: Simd<i8, 64> = black_box(Simd::from_array([99; 64]));assert!(IsConsecutive::is_consecutive(a));assert!(!IsConsecutive::is_consecutive(ninety_nines));

このテクニックを使用すると、タイプとLANESにわたって完全に汎用的なコードを作成できます。次に、これらのアルゴリズムのうち最も速いものをベンチマークして確認する時です。

これがRustにSIMDコードを追加するための最初の6つのルールです。続く第2部では、ルール7から9を見ていきます。これらのルールでは、アルゴリズムの選択とLANESの設定方法、既存のコードへのSIMD演算の統合方法、および(重要なことですが)オプションにする方法について説明します。第2部では、SIMDを使用すべきかどうか、RustのSIMDエクスペリエンスを改善するためのアイデアについても議論します。第2部は近日中に公開されます。皆さんにお会いできることを楽しみにしています。

VoAGIでCarlをフォローしてください。こちらをクリックしてください。私はRustとPythonでの科学プログラミング、機械学習、統計について書いています。一か月に一つの記事を書く傾向にあります。

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