WebAssembly SIMDによる高速な並列アプリケーション

公開日 ・ 更新日 ・ タグ: WebAssembly

SIMDは、Single Instruction, Multiple Data(単一命令、複数データ)の略です。SIMD命令は、複数のデータ要素に対して同時に同じ演算を実行することにより、アプリケーションにおけるデータ並列処理を利用する特殊な命令クラスです。オーディオ/ビデオコーデック、画像処理などの計算集約型のアプリケーションは、SIMD命令を利用してパフォーマンスを向上させるアプリケーションの例です。最新のアーキテクチャのほとんどは、SIMD命令のいくつかのバリアントをサポートしています。

WebAssembly SIMD提案は、最新のアーキテクチャの大部分で使用可能な、移植性が高く、高性能なSIMD演算のサブセットを定義しています。この提案は、SIMD.js提案から多くの要素を派生しており、それはさらに元のDart SIMD仕様から派生しています。SIMD.js提案は、TC39で提案された、SIMD計算を実行するための新しい型と関数を持つAPIでしたが、WebAssemblyでSIMD演算をより透過的にサポートすることに重点を置くため、アーカイブされました。WebAssembly SIMD提案は、ブラウザが基盤となるハードウェアを使用してデータレベルの並列処理を利用する方法として導入されました。

WebAssembly SIMD提案 #

WebAssembly SIMD提案の最上位目標は、移植可能なパフォーマンスを保証する方法で、WebAssembly仕様にベクトル演算を導入することです。

SIMD命令のセットは大きく、アーキテクチャによって異なります。WebAssembly SIMD提案に含まれる演算のセットは、幅広いプラットフォームで十分にサポートされており、パフォーマンスが証明されている演算で構成されています。このため、現在の提案は、固定幅128ビットSIMD演算の標準化に限定されています。

現在の提案では、新しいv128値型と、この型を操作するいくつかの新しい演算が導入されています。これらの演算を決定するために使用された基準は次のとおりです。

  • これらの演算は、複数の最新のアーキテクチャで十分にサポートされている必要があります。
  • パフォーマンスの向上は、命令グループ内の複数の関連アーキテクチャで肯定的である必要があります。
  • 選択された演算セットは、パフォーマンスの低下(もしあれば)を最小限に抑える必要があります。

この提案は現在最終段階(フェーズ4)にあり、V8とツールチェーンの両方で動作する実装があります。

SIMDサポートの有効化 #

機能検出 #

まず、SIMDは新しい機能であり、WebAssemblyをサポートするすべてのブラウザでまだ利用できるわけではないことに注意してください。新しいWebAssembly機能をサポートするブラウザについては、webassembly.orgのウェブサイトをご覧ください。

すべてのユーザーがアプリケーションをロードできるようにするには、SIMDを有効にしたバージョンと有効にしていないバージョンの2つの異なるバージョンを作成し、機能検出の結果に応じて対応するバージョンをロードする必要があります。実行時にSIMDを検出するには、wasm-feature-detectライブラリを使用し、次のように対応するモジュールをロードします。

import { simd } from 'wasm-feature-detect';

(async () => {
const hasSIMD = await simd();
const module = await (
hasSIMD
? import('./module-with-simd.js')
: import('./module-without-simd.js')
);
// …now use `module` as you normally would
})();

SIMDサポート付きのコードの構築については、下記のセクションを確認してください。

ブラウザでのSIMDサポート #

WebAssembly SIMDサポートは、Chrome 91以降でデフォルトで使用できます。下記のように、最新のツールチェーンのバージョンを使用し、仕様の最終バージョンをサポートするエンジンを検出するために最新のwasm-feature-detectを使用してください。何か問題がある場合は、バグを報告してください。

WebAssembly SIMDは、Firefox 89以降でもサポートされています。

SIMDサポートによるビルド #

SIMDをターゲットとするC/C++のビルド #

WebAssemblyのSIMDサポートは、WebAssembly LLVMバックエンドを有効にした最新のclangビルドを使用することに依存しています。EmscriptenもWebAssembly SIMD提案をサポートしています。SIMD機能を使用するには、emsdkを使用して、Emscriptenのlatestディストリビューションをインストールしてアクティブ化します。

./emsdk install latest
./emsdk activate latest

アプリケーションをSIMDを使用するように移植する際に、SIMDコードの生成を有効にする方法はいくつかあります。最新のアップストリームEmscriptenバージョンをインストールしたら、Emscriptenを使用してコンパイルし、-msimd128フラグを渡してSIMDを有効にします。

emcc -msimd128 -O3 foo.c -o foo.js

WebAssemblyを使用するように既に移植されたアプリケーションは、LLVMの自動ベクトル化最適化のおかげで、ソースコードを変更せずにSIMDから恩恵を受けることができます。

これらの最適化により、各反復で算術演算を実行するループを、SIMD命令を使用して一度に複数の入力で同じ算術演算を実行する同等のループに自動的に変換できます。LLVMの自動ベクトル化器は、-msimd128フラグが指定されている場合、最適化レベル-O2-O3でデフォルトで有効になります。

たとえば、2つの入力配列の要素を乗算して結果を出力配列に格納する次の関数について考えてみます。

void multiply_arrays(int* out, int* in_a, int* in_b, int size) {
for (int i = 0; i < size; i++) {
out[i] = in_a[i] * in_b[i];
}
}

-msimd128フラグを渡さないと、コンパイラは次のWebAssemblyループを出力します。

(loop
(i32.store
… get address in `out` …
(i32.mul
(i32.load … get address in `in_a` …)
(i32.load … get address in `in_b` …)

)

しかし、-msimd128フラグを使用すると、自動ベクトル化器はこれを次のループを含むコードに変換します。

(loop
(v128.store align=4
… get address in `out` …
(i32x4.mul
(v128.load align=4 … get address in `in_a` …)
(v128.load align=4 … get address in `in_b` …)

)
)

ループ本体の構造は同じですが、ループ本体内で一度に4つの要素をロード、乗算、格納するためにSIMD命令が使用されています。

コンパイラによって生成されるSIMD命令をより詳細に制御するには、wasm_simd128.hヘッダーファイルを含めます。これは、イントリンシックのセットを定義します。イントリンシックは、呼び出されると、コンパイラによって対応するWebAssembly SIMD命令に変換される特殊な関数です(さらに最適化できる場合を除く)。

例として、前に示したのと同じ関数を、SIMDイントリンシックを使用して手動で書き直したものを次に示します。

#include <wasm_simd128.h>

void multiply_arrays(int* out, int* in_a, int* in_b, int size) {
for (int i = 0; i < size; i += 4) {
v128_t a = wasm_v128_load(&in_a[i]);
v128_t b = wasm_v128_load(&in_b[i]);
v128_t prod = wasm_i32x4_mul(a, b);
wasm_v128_store(&out[i], prod);
}
}

この手動で書き直されたコードは、入力および出力配列がアラインメントされており、エイリアスがなく、サイズが4の倍数であることを前提としています。自動ベクトル化器はこれらの仮定を行うことができず、それらが満たされていない場合を処理するための追加のコードを生成する必要があるため、手書きのSIMDコードは、自動ベクトル化されたSIMDコードよりも小さくなることがよくあります。

既存のC/C++プロジェクトのクロスコンパイル #

多くの既存のプロジェクトは、他のプラットフォームをターゲットにするときにSIMDを既にサポートしています。特に、x86/x86-64プラットフォームではSSEおよびAVX命令、ARMプラットフォームではNEON命令です。これらは通常、2つの方法で実装されています。

1つ目は、SIMD演算を処理し、ビルドプロセス中にC/C++とリンクされるアセンブリファイルを使用する方法です。アセンブリ構文と命令はプラットフォームに依存し、移植性がないため、SIMDを使用するには、このようなプロジェクトでWebAssemblyを追加のサポート対象として追加し、WebAssemblyテキスト形式または上記で説明されているイントリンシックを使用して対応する関数を再実装する必要があります。

もう1つの一般的な方法は、C/C++コードから直接SSE/SSE2/AVX/NEONイントリンシックを使用する方法であり、ここでEmscriptenが役立ちます。Emscriptenはこれらの命令セットすべてに互換性のあるヘッダーとエミュレーションレイヤーを提供し、可能な場合はそれらをWasmイントリンシックに直接コンパイルするエミュレーションレイヤー、そうでない場合はスカラー化されたコードを提供します。

このようなプロジェクトをクロスコンパイルするには、まずプロジェクト固有の構成フラグ(例:./configure --enable-simd)を使用してSIMDを有効にし、コンパイラに-msse-msse2-mavx、または-mfpu=neonを渡し、対応するイントリンシックを呼び出します。次に、CFLAGS=-msimd128 make … / CXXFLAGS="-msimd128 make …"を使用するか、Wasmをターゲットにする際にビルド構成を直接変更して、WebAssembly SIMDも有効にするために-msimd128を追加で渡します。

SIMDをターゲットとするRustのビルド #

RustコードをWebAssembly SIMDをターゲットにコンパイルする場合は、上記のエミュレーターと同様に、simd128 LLVM機能を有効にする必要があります。

rustcフラグを直接、または環境変数RUSTFLAGSを介して制御できる場合は、-C target-feature=+simd128を渡します。

rustc … -C target-feature=+simd128 -o out.wasm

または

RUSTFLAGS="-C target-feature=+simd128" cargo build

Clang/Emscriptenと同様に、simd128機能が有効になっている場合、最適化されたコードに対してLLVMの自動ベクトル化器がデフォルトで有効になります。

たとえば、上記のmultiply_arraysのRust相当の例

pub fn multiply_arrays(out: &mut [i32], in_a: &[i32], in_b: &[i32]) {
in_a.iter()
.zip(in_b)
.zip(out)
.for_each(|((a, b), dst)| {
*dst = a * b;
});
}

は、入力のアラインメントされた部分に対して同様の自動ベクトル化されたコードを生成します。

SIMD演算を手動で制御するには、ナイトリーツールチェーンを使用し、Rust機能wasm_simdを有効にし、std::arch::wasm32名前空間から直接イントリンシックを呼び出します。

#![feature(wasm_simd)]

use std::arch::wasm32::*;

pub unsafe fn multiply_arrays(out: &mut [i32], in_a: &[i32], in_b: &[i32]) {
in_a.chunks(4)
.zip(in_b.chunks(4))
.zip(out.chunks_mut(4))
.for_each(|((a, b), dst)| {
let a = v128_load(a.as_ptr() as *const v128);
let b = v128_load(b.as_ptr() as *const v128);
let prod = i32x4_mul(a, b);
v128_store(dst.as_mut_ptr() as *mut v128, prod);
});
}

あるいは、さまざまなプラットフォームでのSIMD実装を抽象化するpacked_simdのようなヘルパークレートを使用します。

魅力的なユースケース #

WebAssembly SIMD提案は、オーディオ/ビデオコーデック、画像処理アプリケーション、暗号化アプリケーションなどの高計算アプリケーションを高速化することを目指しています。現在、WebAssembly SIMDは、HalideOpenCV.jsXNNPACKなどの広く使用されているオープンソースプロジェクトで実験的にサポートされています。

興味深いデモは、Google ResearchチームによるMediaPipeプロジェクトから提供されています。

説明によると、MediaPipeは、マルチモーダル(例:ビデオ、オーディオ、任意の時系列データ)の適用済みMLパイプラインを構築するためのフレームワークです。そして、Webバージョンもあります!

SIMDがもたらすパフォーマンスの違いを容易に観察できる、最も視覚的に魅力的なデモの1つは、ハンドトラッキングシステムのCPUのみ(GPUなし)のビルドです。SIMDなしでは、最新のラップトップで約14〜15 FPS(フレーム/秒)しか得られませんが、Chrome CanaryでSIMDを有効にすると、38〜40 FPSでよりスムーズなエクスペリエンスが得られます。

SIMDを利用して滑らかな体験を実現する、もう一つの興味深いデモセットは、WebAssemblyにもコンパイル可能な人気のコンピュータビジョンライブラリであるOpenCVから提供されています。リンクからアクセスできます。または、下記の録画済みバージョンをご覧ください。

カード読み取り
透明マント
絵文字置換

今後の取り組み #

現在の固定幅SIMD提案はフェーズ4にあるため、完了とみなされています。

今後のSIMD拡張機能に関するいくつかの調査は、Relaxed SIMDFlexible Vectorsの提案で開始されており、執筆時点ではフェーズ1にあります。