SIMD Vectors
01 何が問題だったのか
現代のほぼすべての CPU は SIMD(Single Instruction, Multiple Data)命令を持っていて、データ並列な処理では通常のスカラ実装の 2〜10 倍の性能を引き出せます。また、グラフィックスや画像処理、AR/VR、コンピュータビジョン、GPU とのデータ受け渡しといった分野では 2〜4 要素の短いベクトル・行列型が Int や Array と同じレベルの基礎的なデータ型として日常的に使われます。にもかかわらず、Swift の標準ライブラリにはこれらを表す共通の型がありませんでした。
これまでの選択肢の問題
SIMD を扱うための既存のアプローチはいずれも不満が残るものでした。
- アセンブリ: 望み通りのコードを書けますが、ターゲットごとに別言語を書くことになり、レジスタ割り付けや呼び出し規約などコンパイラが面倒を見てくれる部分まで自分で扱う必要があります。
- Intrinsics:
__m128(x86)やint8x8_t(ARM)のようなベンダ提供の C の型。言語への後付けであるため、サイズやアラインメントに関する言語仕様と噛み合わず、たとえば C++ コンテナと組み合わせるだけでバグを踏みやすいなどの問題があります。さらに特定アーキテクチャに縛られるため、C/C++ という本来ポータブルな言語で書きながらポータビリティを失います。 - ベクトルクラスライブラリ:
a + bのように演算子で書けるようになる一方、多くは x86 専用などアーキテクチャ固定です。Apple の<simd/simd.h>は Apple が使う全アーキテクチャを網羅しているため実質的にポータブルに使えますが、Apple プラットフォーム外には広がっていません。 - 自動ベクトル化: 単純な問題では機能しますが、一般的な処理ではまだ十分ではなく、明示的なベクトル化が必要になる場面が残ります。
標準ライブラリに型がないことの弊害
各ライブラリ・各アプリが独自に構造体を定義するため、サイズ・アラインメント・提供される演算が微妙に食い違い、ライブラリ間で変換シムを書き続けなければなりません。GPU とのデータ受け渡しや幾何計算、画像処理のような「型が共通でさえあれば相互運用できるはず」の場面でも、共通の地盤がないために毎回手間がかかっていました。
ベクトル型を扱いやすくするためには、単に <simd/simd.h> の機能を移すだけでは足りず、ジェネリックプログラミングへの対応、ベクトルの並べ替えやアラインメントされていないロード、同じ要素数のベクトル型間のキャスト(通常のベクトルライブラリでは関数呼び出しになる)といった、C では難しかった操作も一級の構文で扱えるようにしたいという要求がありました。
02 どのように解決されるのか
標準ライブラリに SIMD2<T>、SIMD3<T>、SIMD4<T>、SIMD8<T>、SIMD16<T>、SIMD32<T>、SIMD64<T> というジェネリックな SIMD ベクトル型を追加します。要素型 T は SIMDScalar プロトコルに適合する必要があり、標準ライブラリの整数型と浮動小数点型(Float80 を除く)がすべて適合します。
基本的な使い方
コンストラクタで要素を並べるか、同じ値で埋めるかでベクトルを作れます。要素へのアクセスは添字、4 要素版には x / y / z / w プロパティも生えています。
let v = SIMD4<Float>(1, 2, 3, 4)
let zeros = SIMD4<Float>(repeating: 0)
let a = v[0] // 1
let b = v.y // 2
算術演算子はそのまま要素ごとの演算になり、スカラとベクトルの混合も書けます。浮動小数点版では + - * / と積和 addingProduct、平方根 squareRoot()、丸め rounded(_:) などが、整数版ではビット演算、オーバーフロー付き演算子(&+ &- &* &<< &>>)、/ % が提供されます。
let x = SIMD4<Float>(1, 2, 3, 4)
let y = SIMD4<Float>(10, 20, 30, 40)
let sum = x + y // SIMD4<Float>(11, 22, 33, 44)
let scaled = 2 * x + 1 // SIMD4<Float>(3, 5, 7, 9)
let fused = x.addingProduct(y, x) // x + y * x
4 要素ベクトルは lowHalf / highHalf / evenHalf / oddHalf で 2 要素ベクトルに分解・再構成でき、アンインタリーブのような操作が言語に組み込まれた形で書けます。
要素ごとの比較とマスク
通常の == と != はベクトル全体の Bool を返しますが、要素ごとに比較したい場合は .-プレフィックスの .== .!= .< .<= .>= .> を使います。結果は SIMDMask と呼ばれる真偽値ベクトルになります。
let x = SIMD4<Int>(1, 2, 3, 4)
let y = SIMD4<Int>(3, 2, 1, 0)
let m = x .== y // SIMDMask: (false, true, false, false)
マスクは .& .| .^ .! で論理演算できます。&& / || と違ってショートサーキットしないため、通常のブール演算子とは別に .-プレフィックス版が用意されています。
マスクの実態はハードウェア事情により「真偽値のベクトル」ではなく「比較対象と同じ幅の整数ベクトル」で表現されます。そのため SIMD4<Bool> は存在せず、代わりに SIMDMask<SIMD4<Int32>> のような型が SIMDScalar の MaskStorage を介して得られます。利用者は通常この具象型名を意識する必要はありません。
マスクによる要素の置き換え
マスクと組み合わせて、特定のレーンだけ値を差し替える replace(with:where:) / replacing(with:where:) が提供されます。分岐のない条件選択として使えます。
var v = SIMD4<Float>(1, -2, 3, -4)
let negatives = v .< 0
v.replace(with: 0, where: negatives) // (1, 0, 3, 0)
let clamped = v.replacing(with: 10, where: v .> 10)
プロトコル階層
型は 2 段構えのプロトコルで設計されています。
SIMDStorageは「要素型Scalar、要素数scalarCount、添字アクセス、ゼロ初期化」だけを要求する最小のプロトコルです。SIMDScalarはこのSIMDStorageをSIMD2Storage…SIMD64Storageとして自分用に関連型で束ねます。SIMDはSIMDStorageをHashableCustomStringConvertibleとともに拡張し、比較やマスク、初期化、算術といった大半の API をextensionとして提供します。
ほとんどの操作はプロトコルの extension として要素ごとのループで実装されていて、@_semantics アノテーションを通じて SIL、LLVM IR のベクトル命令へと段階的に下げられます。そのため、利用者側のコードはあくまで型安全で読みやすい Swift のまま書けます。
リテラル初期化とランダム生成
配列リテラルや任意の Sequence から初期化できます(要素数が一致しないとランタイムで失敗します)。
let v: SIMD4<Int> = [1, 2, 3, 4]
let w = SIMD4<Int>([10, 20, 30, 40])
整数ベクトル・浮動小数点ベクトルにはそれぞれ範囲からの random(in:) が用意され、マスクにも一様乱数を返す random() があります。
C とのインポート
Clang の拡張ベクトル型(extvector)のうち、要素型が SIMDScalar に対応し、要素数が 2, 3, 4, 8, 16, 32, 64 のものは、対応する標準ライブラリのベクトル型として自動的にインポートされます。<simd/simd.h> を使った既存 C API もシームレスに扱えます。
今後の展望
この提案で導入されるのは言語レベルで ABI 安定化に必要な土台部分であり、以降は additive な提案でより豊富な演算を追加していくことが想定されています。スカラ列からベクトルを取り出して反復する仕組みや、SoA と AoS の間の変換(行列転置、インタリーブ/アンインタリーブ)などが今後の課題として挙げられていますが、これらはまだ方向性を示すもので、導入の形や時期を約束するものではありません。