HN 日本語サマリー

← 一覧へ戻る
プログラミング

RustにおけるSIMDの現状(2026年)

The state of SIMD in Rust in 2026 (shnatsel.github.io)

209 pointsby verdagon65 コメント

要約

この記事は、RustにおけるSIMD(Single Instruction, Multiple Data)技術の2026年時点での進捗状況を概観します。自動ベクトル化、ポータブルSIMD抽象化、プラットフォーム固有のイントリンシックといった主要なアプローチを解説し、命令セットの互換性や浮動小数点演算の精度といった課題、そして利用可能なライブラリについて掘り下げています。

全文翻訳

昨年から多くの進歩があり、私もその一部を担いました。 去年の調査の後、最も有望に見えたSIMDライブラリへの貢献を始めました。それが縁で、今ではFearless SIMDのメンテナーになっています。 利益相反を避けるため、他のライブラリ(std::simd、wide、pulp、macerator)の作者にこの記事のドラフトのレビューとフィードバックを依頼しました。しかし、編集権は私にあり、すべての間違いは私の責任です。 今年の調査は、以前のものよりも詳細です。覚悟して、最初から見ていきましょう。 SIMDとは何か? なぜSIMDか? 算術演算を行うハードウェアは安価であり、今世紀のCPUには豊富に搭載されています。しかし、命令デコードブロックは1つしかなく、それを高速に動作させるのは難しいため、算術ハードウェアは大幅に過小評価されています。 命令デコードのボトルネックを回避するために、CPUに一度に多数の数値のバッチを供給し、加算のような単一の算術演算を実行させることができます。これが「Single Instruction, Multiple Data」、略してSIMDの名前の由来です。 2つの数値を加算する代わりに、数値の2つのバッチ、つまり「ベクトル」を加算でき、これは単一の加算を行うのとほぼ同じ時間がかかります。 最近のx86チップでは、これらのバッチは最大512ビットのサイズになるため、理論上はf64の演算で8倍、u8の演算で64倍の速度向上が期待できます。実際には、それより遅くなることも速くなることもあります。 命令セット 歴史的に、SIMD命令はCPUアーキテクチャが設計された後に追加されたため、SIMDは各アーキテクチャに独自のマーケティング名を持つ拡張機能となっています。 ARMは「NEON」と呼んでおり、すべての64ビットARM CPUに搭載されています。 WebAssemblyにはマーケティング部門がないため、「WebAssembly 128-bit packed SIMD extension」と呼んでいます。 64ビットx86は「SSE2」と呼ばれるものと共に登場しましたが、その後、多くの拡張機能が追加されました。SSE 4.2はより多くの演算を追加し、AVXとAVX2は256ビットベクトルを追加し、AVX-512は512ビットベクトルとさらに多くの演算を追加しました。 上記の段落の「後で」という言葉が問題を引き起こします。 このCPUはその命令を持っているか? x86_64 CPUでプログラムを実行している場合、そのCPUが特定のSIMD拡張機能を持っているとは限りません。そのため、デフォルトではコンパイラはSSE2を超える命令の使用を許可されていません。なぜなら、それはすべてのx86_64 CPUで動作しないからです。 この問題には2つの回避策があります。 自社のサーバーやパブリッククラウドでのみバイナリを実行する会社で働いている場合、10年以上前に導入された少なくともAVX2を搭載している十分新しいものであると断言し、AVX2を持たないもの上で実行された場合にプログラムがクラッシュまたは誤動作するようにすることができます。 RUSTFLAGS='-C target-cpu=x86-64-v3' cargo build --release しかし、他の人が実行するためにバイナリを配布している場合、それは実際には選択肢ではありません。 代わりに、関数マルチバージョン化と呼ばれることを行うことができます。同じ関数を異なるSIMD拡張機能のために複数回コンパイルし、プログラムが実際に実行されるときに、CPUがどの機能をサポートしているかを確認し、それに基づいて適切なバージョンを選択します。 幸いなことに、この問題はx86にのみ存在します。ARMは64ビットCPUでNEONを必須とし、その後有用なSIMD拡張機能を追加していません(詳細は後述)。WebAssemblyでは、SIMDありとSIMDなしの2つの異なるバイナリをコンパイルし、JavaScriptを使用してブラウザがSIMDをサポートしているかを確認する必要があります。 SIMDをどう使うか? SIMDを活用するには3つの方法があります。 自動ベクトル化: &[i32].sum() ポータブルSIMD抽象化: i32x4 + i32x4 プラットフォーム固有のイントリンシック - 少しお待ちください、もっと大きなコードブロックが必要になります: #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"),target_feature = "sse2"))] _mm_add_epi32(__m128i , __m128i) #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] vaddq_u32(int32x4_t, int32x4_t) それぞれが何を意味し、各プログラミングモデルの状態はどうなっているかを見てみましょう。 自動ベクトル化 プレーンなRustを書いて、コンパイラのヒューリスティックに任せましょう! コンパイラが確実に(あるいはそれに近い形で)ベクトル化できるようなコードの書き方に注意すれば、かなりうまく機能させることができます。これには通常、&[i32]ではなく&[i32].as_chunks()をイテレートし、アセンブリをベンチマークしたり凝視したりして、それが機能したことを確認することが含まれます。詳細は「Can You Trust a Compiler to Optimize Your Code?」を参照してください。 これは最も簡単なオプションであり、依存関係を必要とせず、コンパイラがサポートするどんなにマイナーな命令セットでも自動的にサポートします。 欠点は、この方法があまり信頼できないことです。関数が大きくて複雑になるほど、コンパイラがそれをベクトル化できない可能性が高くなります。コンパイラバージョンや周囲のコードの変更によって、パフォーマンスが大きく変動することもあります。 浮動小数点型にも特別な注意が必要です。 浮動小数点数は奇妙です。たとえ配列の浮動小数点数を合理的な精度で合計するような単純なことでも、驚くほど複雑になります。「Taming Floating-Point Sums」を参照してください。 以前は、自動ベクトル化は浮動小数点型では機能しませんでした。なぜなら、結果の精度が変わってしまうからです(多くの場合、より良い結果になりますが、コンパイラは観測可能な結果を変更することは許可されていません)。 これはRust 1.98で変更され、代数演算(algebraic_add()など)が安定化され、コンパイラが観測可能な結果を変更できるようになりました。これは-ffast-mathよりも安全な方法です。ほとんどの場合、ベクトル化の対象となるためには、コードを書き直してそれらを使用する必要があります。 そして、まだマルチバージョン化を何らかの方法で実現する必要があります。なので、ついでに見てみましょう。 'multiversion' クレート 以下で議論するオールインワンSIMDクレートもマルチバージョン化を提供しますが、自動ベクトル化に最も役立つmultiversionを簡単に見てみましょう。 使い方は非常に簡単です。関数の上に#[multiversion(targets = "simd")]アノテーションを追加するだけで完了です。 しかし、その簡単さの裏には、文書化されていない落とし穴があります。#[multiversion]でアノテーションされた関数を呼び出すと、わずかなオーバーヘッドが発生します。非常に小さいですが、12命令未満ですが、対象の関数自体が非常に小さい場合、顕著なオーバーヘッドとして現れます。 経験則として、関数にループが含まれている場合は#[multiversion]を追加し、少数の値を処理する場合は#[inline(always)]を追加し、呼び出しチェーンのどこかに#[multiversion]があることを確認してください。 以下にリストされている他のクレートにはこの落とし穴がなく、より多くのボイラープレートのコストで、関数のサイズについて考える必要がありません。 multiversionは、定義済みのSIMDレベルを選択するのではなく、必要なCPU拡張機能を正確にリストできる唯一のクレートです。そのため、コードが非常に新しい命令から恩恵を受ける場合、それを選択できます。しかし、私の経験では、自動ベクトル化されたコードではこれはほとんど起こりません。 AVX-512の場合、multiversionはそれが存在するかどうかをチェックしますが、実際に高速かどうかはチェックしません。これは実際にはパフォーマンスを低下させる可能性があります(詳細は後述)。ボイラープレートのコストをかけてこれを回避できます。各関数にこれを記述する必要があります。 #[multiversion::multiversion(targets( "x86_64+cmpxchg16b+popcnt+sse3+sse4.1+sse4.2+ssse3", // x86_64-v2 "x86_64+avx+avx2+bmi1+bmi2+cmpxchg16b+f16c+fma+lzcnt+movbe+popcnt+sse3+sse4.1+sse4.2+ssse3+xsave", // x86_64-v3 "x86_64+fxsr,adx,avx512bitalg,avx512bw,avx512cd,avx512dq,avx512f,avx512ifma,avx512vbmi,avx512vbmi2,avx512vl,avx512vnni,avx512vpopcntdq,bmi1,bmi2,cmpxchg16b,fma,gfni,lzcnt,movbe,pclmulqdq,popcnt,vpclmulqdq,xsave,xsavec,xsaveopt,xsaves", // Ice Lake and later )]] ポータブルSIMD抽象化 プロダクションレディなものがいくつかあります。望ましい機能は次のとおりです。 固定幅ベクトル: f32x4、u8x16などの用語でコードを記述する(既知のサイズ) ハードウェア幅ベクトル: ハードウェアがサポートする最大のベクトルサイズを、事前に知ることなく使用する 要素型に対するジェネリック: f32x4とf64x2の両方で機能するコードを記述する ベクトル幅に対するジェネリック: f32x4、f32x8、f32x16など、すべての幅で機能するコードを記述する TL;DRテーブル: std::simd (nightly) fearless simd wide pulp macerator multiversioning 📦/🛠️ ✅ ❌ ✅ ✅ ✅ fixed-width vectors ✅ ✅ ✅ ☑️ ❌