Swift Digest
SE-0251 | Swift Evolution

SIMD additions

Proposal
SE-0251
Authors
Stephen Canon
Review Manager
John McCall
Status
Implemented with Modifications (Swift 5.1)

01 何が問題だったのか

Swift 5 で導入された SIMD 型とプロトコルは、短い固定長ベクトルを扱う土台として機能し始めましたが、早期の利用者からはいくつか物足りない点が報告されていました。具体的には、ベクトルを作る前にサイズを静的に知りたい、3 次元と 4 次元を行き来する同次座標の扱いを簡潔に書きたい、シェーダ言語のように要素を自由に並び替えたい、ベクトルを 1 つのスカラーにまとめる横方向の演算(reduction)が欲しい、要素ごとの minmaxclamp が欲しい、真偽ベクトルに対する anyall が欲しい、.zero と対になる .one が欲しい、といった要望です。

また、当初のレビュー時には時間的制約で判断を保留した機能もあり、それらをまとめて再検討する必要がありました。個別の機能はどれも小さなものですが、SIMD で実用的なコードを書くうえでは不足していると不便で、利用者は自前の拡張で埋めざるを得ない状況でした。このProposalは、そうした「SIMD の穴埋め」的な追加機能をまとめて整理し、標準ライブラリとして提供することを目的としています。

02 どのように解決されるのか

このProposalでは、SIMD 関連の機能を 7 つのテーマに分けて追加しています。いずれも純粋な追加であり、既存コードへの影響はありません。

静的な scalarCount

要素数 scalarCount はインスタンスプロパティとして定義されていましたが、ベクトルを構築する前(特にイニシャライザ内)に要素数を検証したい場面があります。そこで、同じ値を static プロパティとしても参照できるようにします。すべての SIMD 型が同じ仕組みで取得できるよう、既存のインスタンスプロパティを利用した形で定義されています。

extension SIMDStorage {
  public static var scalarCount: Int {
    return Self().scalarCount
  }
}

let n = SIMD4<Float>.scalarCount // 4

ベクトルの拡張

グラフィックスで同次座標を扱うときなど、SIMD3SIMD4SIMD2SIMD3 の間を行き来したい場面が頻繁にあります。そのための初期化子が追加されました。

extension SIMD3 {
  public init(_ xy: SIMD2<Scalar>, _ z: Scalar)
}

extension SIMD4 {
  public init(_ xyz: SIMD3<Scalar>, _ w: Scalar)
}

let v = SIMD3<Float>(1, 2, 3)
let p = SIMD4(v, 1) // SIMD4<Float>(1, 2, 3, 1)

appending(_:) のようなメソッド形式ではなくイニシャライザにしたのは、戻り値の型が変わること、および SIMD5 のような型が標準ライブラリには存在せず一般化しにくいことが理由です。

スウィズル(任意の要素並び替え)

シェーダ言語や Clang の拡張でおなじみの v.zyx のような要素並び替えを、添字(subscript)として提供します。セッタにはしない代わりに、インデックス自体を SIMD ベクトルで動的に指定できる のが特徴です。インデックスは要素数で剰余が取られるため、範囲外で実行時トラップすることはありません。

extension SIMD {
  public subscript<Index>(index: SIMD2<Index>) -> SIMD2<Scalar>
  where Index: FixedWidthInteger
  // SIMD3, SIMD4 版も同様に用意される
}

let v = SIMD4<Float>(1, 2, 3, 4)
let xyz = SIMD3(2, 1, 0)
let w = v[xyz] // SIMD3<Float>(3, 2, 1)

横方向の reduction

ベクトル全体を 1 つのスカラーに畳み込む操作が追加されます。内積や行列積など、SIMD コードの最後で 1 スカラーに落とし込む場面で使います。

extension SIMD where Scalar: Comparable {
  public func min() -> Scalar
  public func max() -> Scalar
}

extension SIMD where Scalar: FixedWidthInteger {
  // オーバーフローをラップして加算する
  public func wrappedSum() -> Scalar
}

extension SIMD where Scalar: FloatingPoint {
  public func sum() -> Scalar
}

整数版が sum ではなく wrappedSum なのは、ベクトル上の &+ と同じく、トラップする通常の sum を将来追加する余地を残すためです。浮動小数点の sum は、再現性と性能のバランスをとるためバイナリツリーでの合計として実装される予定です。

anyall

SIMDMask(真偽ベクトル)に対して、少なくとも 1 要素が真かどうかを返す any、すべての要素が真かどうかを返す all が追加されます。これらはメソッドではなく自由関数として提供されます。比較結果のような名前のない値に対して使われることが多く、any(x .< 0) のように先頭に置いたほうが読みやすいためです。

public func any<Storage>(_ mask: SIMDMask<Storage>) -> Bool
public func all<Storage>(_ mask: SIMDMask<Storage>) -> Bool

if any(x .< minValue .| x .> maxValue) {
  // 特別処理
}

要素ごとの min / max / clamp

SIMD には要素ごとの算術演算はありましたが、要素ごとの minmax がありませんでした。値を指定範囲に収める clamp と合わせて追加されます。浮動小数点ベクトルの clamp では .nanlowerBound に置き換えられます。

extension SIMD where Scalar: Comparable {
  public mutating func clamp(lowerBound: Self, upperBound: Self)
  public func clamped(lowerBound: Self, upperBound: Self) -> Self
}

要素ごとの min / max を提供する自由関数も追加されます。ただし受け入れ時の修正として、Comparable 上の min(_:_:) と衝突し得ることから、SIMD 版は pointwiseMin(_:_:)pointwiseMax(_:_:) という名前で採用されました。

public func pointwiseMin<V>(_ lhs: V, _ rhs: V) -> V
  where V: SIMD, V.Scalar: Comparable
public func pointwiseMax<V>(_ lhs: V, _ rhs: V) -> V
  where V: SIMD, V.Scalar: Comparable

let a = SIMD4<Float>(1, 5, 3, 2)
let b = SIMD4<Float>(4, 2, 3, 6)
let m = pointwiseMin(a, b) // SIMD4<Float>(1, 2, 3, 2)

.one

SIMD 型は型推論の曖昧さを避けるため ExpressibleByIntegerLiteral には適合しません。.zero と同様の利便性プロパティとして .one が追加されます。

extension SIMD where Scalar: ExpressibleByIntegerLiteral {
  public static var one: Self {
    return Self(repeating: 1)
  }
}

let ones = SIMD4<Float>.one // (1, 1, 1, 1)