Apple Engine

Apple, iPhone, iOS, その周辺のことについて

Swift 5 で導入された SIMD Vector Types

単体の simd や Accelerate フレームワークの simd ライブラリなどの一部が Swift 5 で使用できる様になった。
ちなみにクォータニオンはなく、関数も一部しかないので、 あくまでもデータを保存する型の様なものだと思ってもらえれば良い。

 

SIMD とは

以前もこのブログで記事にしているが、雑に説明すると複数のデータ(レーン)を1回の処理で実行する CPU で行う並列化の形。

通常は SISD の形をとっており変数単位で計算を行うが、
SIMD は画像のように [X,Y,Z,W] の計算を1度で行う事ができ高速かつ記述が簡単である。

SISD と比較すると2倍から10倍の速さになると予想される。

f:id:x67x6fx74x6f:20180515183839p:plain

appleengine.hatenablog.com

 

Swift Standard Library の SIMD Vector Types とは?

Swift > Math > Vector > SIMD に存在しており、
SIMD Vector Types の説明を直訳すると「サイズが異なる固定幅の数値型の固定幅ベクトルを処理する」とのこと。

developer.apple.com

 

SIMD Vector Types を使ってみる

4つの Float を指定した 2 レーンに数字を入れて足してみる。
SIMD の型を受け取れるものなら、ひとつずつ格納、計算せずに、一気に計算し渡すことができる。

let f4_1 = SIMD4<Float>(0.0, 1.0, 2.0, 3.0)
let f4_2 = SIMD4<Float>(1.0, 2.0, 3.0, 4.0)

let result = f4_1 + f4_2 // SIMD4<Float>(1.0, 3.0, 5.0, 7.0)

 

SIMD Vector Types の中身

プロトコルの SIMDScalar、SIMDStorage と構造体の SIMDMask で構成されており、 SIMD として使いやすい様に設定されている。

  • SIMDMask は SIMD 同士を比較し返される値を格納する構造体。
  • SIMDStorage プロトコルは、ストレージのレイアウトを定義し、値へのアクセスを持つプロトコル。
  • SIMDScalar は各ストレージとマスクを持つプロトコル
  • Swift で定義されている SIMD は上記のものをまとめたもの

これらを使用し型として SIMD のレーンとして設定することができる。

 

SIMD のレーンとストレージ

Swift で定義されている SIMD の型は、レーンで扱える数が SIMDStorage 定義されており、レーン内の型を SIMDScalar で定義されている。

以下、用意されている SIMD 型。
T はジェネリクスとなっており SIMDStorage で定義されている Scalar となる。

  • SIMD2<T>
  • SIMD3<T>
  • SIMD4<T>
  • SIMD8<T>
  • SIMD16<T>
  • SIMD32<T>
  • SIMD64<T>

 

T に設定できる型は以下のもの
Swift Standard Library で存在している Float80 は使用できない。

  • Int
  • Int8
  • Int16
  • Int32
  • Int64
  • UInt
  • UInt8
  • UInt16
  • UInt32
  • UInt64
  • Float
  • Double

 

ちなみに各型で SIMD2 〜 64 が設定されている。
型の方で SIMD3 の定義がない理由は不明。

f:id:x67x6fx74x6f:20190401161027p:plain
Int の SIMDStorage

 

SIMD で用意されている演算子や関数

SIMD では共通の関数があるが、Scalar を以下の分類に分けており、使用できる関数が異なる。

  • FixedWidthInteger
  • FloatingPoint
  • BinaryFloatingPoint

 

演算子と関数の量がそこそこあるので、以下端折って紹介。

 

初期化

var f4_1 = SIMD4<Float>()
// SIMD4<Float>(0.0, 0.0, 0.0, 0.0)

var f4_2 = SIMD4<Float>.zero
// SIMD4<Float>(0.0, 0.0, 0.0, 0.0)

var f4_3 = SIMD4<Float>(0.0, 1.0, 2.0, 3.0)
// SIMD4<Float>(0.0, 1.0, 2.0, 3.0)

var f4_4 = SIMD4<Float>(repeating: 4.0)
// SIMD4<Float>(4.0, 4.0, 4.0, 4.0)

var f4_5 = SIMD4<Float>(0.0, 1.0, 2.0, 3.0)
// SIMD4<Float>(0.0, 1.0, 2.0, 3.0)

var f4_6 = SIMD2<Float>.random(in: Range<Float>.init(uncheckedBounds: (lower: 2.0, upper: 4.0)))
// 2 から 4 の乱数が 2 つ設定される。
// SIMD2<Float>(2.6809106, 2.5394516)

 

中身を取り出す

レーンで使用するため、あまり使用することはないが値をひとつずつ取り出すことができる。
x, y, z, w で取り出すことができるのは SIMD2、SIMD3、SIMD4 だけ。

var f4_1 = SIMD4<Float>(1.0, 2.0, 3.0, 4.0)

f4_1.x // 1
f4_1.y // 2
f4_1.z // 3
f4_1.w // 4

f4_1[0] // 1
f4_1[1] // 2
f4_1[2] // 3
f4_1[3] // 4

 

四則演算と符号

var f4_1 = SIMD4<Float>(1.0, 2.0, 3.0, 4.0)
var f4_2 = SIMD4<Float>(2.0, 2.0, 2.0, 2.0)

f4_1 + f4_2 // SIMD4<Float>(3.0, 4.0, 5.0, 6.0)
f4_1 - f4_2 // SIMD4<Float>(-1.0, 0.0, 1.0, 2.0)
f4_1 * f4_2 // SIMD4<Float>(2.0, 4.0, 6.0, 8.0)
f4_1 / f4_2 // SIMD4<Float>(0.5, 1.0, 1.5, 2.0)

-f4_1 // SIMD4<Float>(-1.0, -2.0, -3.0, -4.0)

 

関数

レーンを掛けた後に値を足す。

addProduct は自身の中身を変え、addingProduct 変更後の値を返す。

var f4_1 = SIMD4<Float>(4.0, 4.0, 4.0, 4.0)
var f4_2 = SIMD4<Float>(1.0, 2.0, 3.0, 4.0)

f4_1.addProduct(f4_1, f4_2)
// f4_1 + SIMD4<Float>(4.0, 8.0, 12.0, 16.0)
// SIMD4<Float>(8.0, 12.0, 16.0, 20.0)

 

レーンの値に√を適応する。(FloatingPoint のみ)
√4 = 2 であるため関数適応後は 2 になっている

var f4_1 = SIMD4<Float>(4.0, 4.0, 4.0, 4.0)

f4_1.formSquareRoot()
// SIMD4<Float>(2.0, 2.0, 2.0, 2.0)

 

ビット NOT。(FixedWidthInteger のみ)

var i2 = SIMD2<UInt8>( 1, 1 )

let result = ~i2 // SIMD2<UInt8>(254, 254)
// UInt8(1)   = 0b00000001
// UInt8(254) = 0b11111110

 

SIMD のマスクとは?

SIMD が行うマスクはレーンを比較して true / false を判別し SIMDMask として返す。

var f4_1 = SIMD4<Float>(1.0, 2.0, 3.0, 4.0)
var f4_2 = SIMD4<Float>(4.0, 4.0, 4.0, 4.0)

var f4_mask = f4_1 .== f4_2
// SIMDMask<SIMD4<Int32>>(false, false, false, true)

 

マスクを使用することができる関数があり、以下の例では 4 つの値を持つレーンを 1.0 で埋め、上記の f4_mask を使用して true の箇所を 4.0 に変更させる。

var result = SIMD4<Float>(repeating: 1.0).replacing(with: 4.0, where: f4_mask)
// SIMD4<Float>(1.0, 1.0, 1.0, 4.0)

 

simd フレームワークとの互換性はあるのか?

simd での typealias が Swift Standard Library の SIMD となったので互換性がある。

public typealias float2 = SIMD2<Float>
public typealias float3 = SIMD3<Float>
public typealias float4 = SIMD4<Float>

public typealias double2 = SIMD2<Double>
public typealias double3 = SIMD3<Double>
public typealias double4 = SIMD4<Double>

 

そのため、今まで返り値は float4 であったが、SIMD4 型で返る様になった。

var f4_1 = SIMD4<Float>(1.0, 2.0, 3.0, 4.0)
var f4_2 = float4(f4_1) // SIMD4<Float>(1.0, 2.0, 3.0, 4.0)

var f4_3 = float4(1.0, 2.0, 3.0, 4.0)
var f4_4 = SIMD4<Float>(f4_3) // SIMD4<Float>(1.0, 2.0, 3.0, 4.0)

 

行列に SIMD 型を渡すこともできる。

var f4x4 = simd_float4x4(
    SIMD4<Float>(1.0, 0.0, 0.0, 0.0),
    SIMD4<Float>(0.0, 1.0, 0.0, 0.0),
    SIMD4<Float>(0.0, 0.0, 1.0, 0.0),
    SIMD4<Float>(0.0, 0.0, 0.0, 1.0)
)

 

もちろん、simd フレームワークの関数を使用することもできる

var f3 = SIMD3<Float>(1.0, 1.0, 0.0)

var length = simd_length(f3)
// x:1、y:1 なので、直角三角形と同じとなり長さを求めた値は √2 (1.414214) になる

 

ARKit や SceneKit でも使用することができるか?

今まで通り simdPosition など simd の付くものへそのまま渡せる。

let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)

cameraNode.simdPosition = SIMD3<Float>(0, 0, 15)
// cameraNode.position = SCNVector3(x: 0, y: 0, z: 15) と同じ

 

まとめ

simd フレームワーク自体、Apple の開発プラットフォーム共通で使用できるため、
SIMD の操作が Swift Standard Library で導入されても直接は関係ないかもしれない。

Intel チップで使用できる様に設計されているはずなので、サーバーサイド Swift など Apple の開発プラットフォームではない環境でも使用できそうな感はある。