「RustコードのSIMDアクセラレーションのための9つのルール(パート1)」
「RustコードにおけるSIMDアクセラレーションのための9つのルール(パート1)」
レンジセットブレイズのデータインジェクションを7倍に増やす際の一般的な教訓
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演算を使用すれば、さらに改善できるのでしょうか?はい!
- 「ゼロから始めるLoRAの実装」
- 地球は平らではなく、あなたのボロノイ図もそうであるべきではありません
- ポイントクラウド用のセグメント化ガイド「Segment Anything 3D for Point Clouds Complete Guide (SAM 3D)」
「重なり合っている」の定義については、以前の記事のルール2を参照してください。また、整数が重なり合っていない場合はどうなるのか?
RangeSetBlaze
はHashSet
よりも2〜3倍遅いです。
重なり合う整数に対して、SIMD演算に基づく新しいRangeSetBlaze::from_slice
メソッドは、RangeSetBlaze::from_iter
よりも7倍速くなります。これにより、HashSet::from_iter
よりも200倍以上速くなります。(整数が重なり合わない場合は、HashSet
よりも2〜3倍遅いです。)
この高速化の過程で、SIMD演算を使用してプロジェクトを加速するための9つのルールを学びました。
これらのルールは以下の通りです:
- 夜間のRustと
core::simd
を使用する – Rustの実験的な標準SIMDモジュール。 - CCC:コンピュータのSIMD能力をチェックし、制御し、選択する。
core::simd
を選択的に学ぶ。- 候補のアルゴリズムを考え出す。
- GodboltとAIを使用してコードのアセンブリを理解する(アセンブリ言語を知らなくても)。
- インラインのジェネリックスによるすべてのタイプとLANESの一般化(うまくいかない場合は)マクロ、(うまくいかない場合は)トレイトを使用する。
これらのルールの詳細については、第2部を参照してください:
7. アルゴリズムを選択するためのCriterionベンチマーキングおよびLANESは(ほとんど)常に32または64であることを発見するために使用する。
8. as_simd
、i128
/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
data
とSimd
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
これは、私のマシンがsse2
、avx2
、およびavx512f
のSIMD拡張をサポートしていることを示しています。 そのうち、デフォルトではRustは普及している20年前のsse2
拡張を有効にしています。
SIMD拡張機能は、avx512f
がavx2
より上で、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のデフォルトではないか疑問に思うかもしれません。問題は、avx2
やavx512f
を使用して作成したバイナリは、それらのSIMD拡張機能がないコンピュータで実行できないということです。したがって、自分自身の使用のためにコンパイルしている場合は、target-cpu=native
を使用します。ただし、他の人のためにコンパイルしている場合は、注意してSIMD拡張機能を選択し、どのSIMD拡張機能レベルを前提としているかを知らせてください。
幸いなことに、選択したSIMD拡張機能のレベルに関係なく、RustのSIMDサポートは非常に柔軟であり、後で簡単に決定を変更することができます。次に、RustでSIMDを使用したプログラミングの詳細を学びましょう。
ルール3: core::simd
を選択的に学ぶ。
Rustの新しいcore::simd
モジュールをビルドするためには、選択したビルディングブロックを学ぶ必要があります。以下は、私が最も有用と考える構造体、メソッドなどのチートシートです。各項目には、それに対するドキュメントへのリンクが含まれています。
構造体
Simd
– 特別な、アラインされた、固定長のSimdElement
の配列。配列内の位置とその位置に格納されている要素を「レーン」と呼びます。デフォルトでは、Simd
の構造体は参照ではなくコピーされます。Mask
– レーンごとの含まれる/含まれないを示す特別なブール配列。
SimdElements
Simd
コンストラクタ
Simd::from_array
– 固定長配列をコピーしてSimd
構造体を作成します。Simd::from_slice
– スライスの最初のLANE
要素をコピーしてSimd<T,LANE>
構造体を作成します。Simd::splat
–Simd
構造体のすべてのレーンに単一の値を複製します。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_lt
、simd_le
、simd_ge
、simd_lt
、simd_eq
、simd_ne
もサポートされています。Simd::rotate_elements_left
– 指定された回数だけSimd
構造体の要素を左に回転させます。また、rotate_elements_right
もあります。simd_swizzle!(simd, indexes)
– 指定された定数インデックスに基づいて、Simd
構造体の要素を並べ替えます。simd == simd
– 2つのSimd
構造体の等価性をチェックし、通常のbool
結果を返します。Simd::reduce_and
–Simd
構造体のすべてのレーンでビットごとのAND演算を行います。また、reduce_or
、reduce_xor
、reduce_max
、reduce_min
、reduce_sum
(ただしreduce_eq
はなし)もサポートされています。
Mask
メソッドと演算子
Mask::select
– マスクに基づいて2つのSimd
構造体から要素を選択します。Mask::all
– マスクがすべてtrue
かどうかを判定します。Mask::any
– マスクにtrue
が含まれるかどうかを判定します。
レーンについてのすべて
Simd::LANES
–Simd
構造体の要素(レーン)の数を示す定数です。SupportedLaneCount
–LANES
の許可される値を示します。ジェネリクスで使用します。simd.lanes
–Simd
構造体のレーン数を示すconstメソッドです。
低レベルのアライメント、オフセットなど
可能な場合は、to_simd
を使用してください。
mem::size_of
、mem::align_of
、mem::align_to
、intrinsics::offset
、pointer::read_unaligned
(unsafe)、pointer::write_unaligned
(unsafe)、mem::transmute
(unsafe、const)
それ以外にも興味があるかもしれないもの
deinterleave
、gather_or
、reverse
、scatter
これらのビルディングブロックが手元に揃っているので、何かを構築する時が来ました。
ルール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を使用するための手順を踏んでください:
- ウェブブラウザでgodbolt.orgを開きます。
- 新しいソースエディタを追加します。
- 言語としてRustを選択します。
- 興味のあるコードを貼り付けます。関数はpublicにしましょう(
pub fn
)。main関数や不要な関数は含めないでください。ツールは外部のクレートをサポートしません。 - 新しいコンパイラを追加します。
- コンパイラのバージョンをnightlyに設定します。
- オプションを設定します(現時点では):
-C opt-level=3 -C target-feature=+avx512f
。 - エラーがあれば、出力を確認します。
- ツールの状態を共有したり保存したりする場合は、「共有」をクリックします
上の画像からわかるように、Splat2とSizzleはまったく同じですので、Sizzleは考慮から外すことができます。もし私のGodboltセッションを開くと、ほとんどの関数がほぼ同じアセンブリ命令にコンパイルされることもわかるでしょう。例外としては、Regular(はるかに長い)とSplat0(初期チェックを含む)があります。
アセンブリで、512ビットのレジスタはZMMから始まります。256ビットのレジスタはYMMから始まります。128ビットのレジスタはXMMから始まります。生成されたアセンブリをよりよく理解するためには、AIツールを使用して注釈を生成します。たとえば、ここではSplat2についてBing Chatに質問しています:
-C target-feature=+avx2
やtarget-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
が必要ない場合、羨ましいです。次のルールに進むこともできます。同様に、家事をするために個人用ロボットを持つのと同じくらい簡単にジェネリックなconstcomparison_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つだけではなく)で実行する必要があります。
幸いなことに、マクロに依存する回避策があります。これは再びマクロに依存します。また、i8
、i16
、i32
、i64
、isize
、u8
、 u16
、u32
、u64
、および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!
Was this article helpful?
93 out of 132 found this helpful
Related articles