SSブログ

SIMDがStandard Libraryに [Swiftプログラミング]

こないだSwiftの勉強で光線追跡エンジンを書き直してる話を書いた。そこでは勉強なのでベクトル行列演算もベタに自分で書いてたけど、Objective-Cで書いたやつではAccelerate frameworkを呼んでいる。よく見るとSwiftのStandard LibraryにSIMDが入っている。知らなかった。

SIMDはもともとCPUのベクトルユニット使うためのライブラリで、昔は確かAccelerate frameworkの一部だったと思う(一番最初は、懐かしいAltiVecを使い倒すために導入されたと覚えている)。整数や浮動小数点数を4つなり8つなりまとめて、ベクトルユニットが可能な演算を定義してあった。

今見ると、Accelerateからは独立してるようである。Swift Standard Libraryに入れるためにそうなったんだろうか。Metalのshaderにも構造体が流用されているようなので、その関係なんだろうか。Swift Standard LibraryのSIMDドキュメントはかなり簡素でこれだけ見てもなかなかよくわからない。たぶんAccelerateにあったやつのラッパになってるんだろうと思う。僕は昔ならいざ知らず今後SIMDを単独で積極的に使うことはあまりないとは思うんだけど、Swiftの勉強になりそうなので詳しく見てみる....

Swift Standard Libraryには
protocol SIMD
protocol SIMDScalar
protocol SIMDStorage
のみっつのprotocolが定義されている。SIMDはベクトル用で、SIMDScalarはその要素用である。SIMDStorageはSIMDなかの要素の配置を定義しているようである。SIMD protocolをconformする構造体として
public struct SIMD◎ : SIMD where Scalar : SIMDScalar
というのが定義されている。◎は要素の数で2、3、4、8、16、32、64がある。つまりSIMD4というと要素の型がScalarで要素が4つの構造体だということになる。SIMDScalarはassociatedtypeが定義されるだけの親のないprotocolだけど、普通のIntやFloatやDoubleがSIMDScalarをconformしてることになってる。

SIMD protocolには基本的な
    public var scalarCount: Int { get }
    public var indices: Range { get }
    public init(repeating value: Self.Scalar)
    public static func == (lhs: Self, rhs: Self) -> Bool
    @inlinable public func hash(into hasher: inout Hasher)
    public func encode(to encoder: Encoder) throws
なんかが定義されている。また、
    @inlinable public static func random(in range: Range, using generator: inout T) -> Self where T : RandomNumberGenerator
のような乱数発生器も定義されている。

SIMDのextensionにSIMDScalarを限定していろいろな演算子が定義されてるけど、何をする演算なのかドキュメントには何も書いてないのがたくさんある。
where Self.Scalar : FixedWidthInteger
のときは
    prefix public static func ~ (rhs: Self) -> Self
    public static func & (lhs: Self, rhs: Self) -> Self
    public static func ^ (lhs: Self, rhs: Self) -> Self
    public static func | (lhs: Self, rhs: Self) -> Self
    public static func &<< (lhs: Self, rhs: Self) -> Self
なんかがあって、そんなもの見りゃわかるだろう、ということらしい。FloatingPointに限定して同じようにたくさん定義されているが、浮動小数点特有の
    public func squareRoot() -> Self
    public func rounded(_ rule: FloatingPointRoundingRule) -> Self
なんていうのもある。

具体的にどう使うかと言うと
    let vec0 = SIMD3.random(in: 0...1)
    let indx = vec0.indices
    let leng = vec0.scalarCount
    let vec1 = SIMD3(arrayLiteral: 1.0, 2.0, 3.0)
    let vec3 = vec1 - vec0
などとできる。乱数発生が並列的に行われるかどうか、はよくわからないが、単純な擬似乱数ならベクトルユニットを使うことができるだろう。そしてvec3の計算には自動的にCPUのベクトルユニットが使われるはずである(確認してないので知らんけど)。vec0には[0,1]の乱数が要素の3次元ベクトルが入る。ちなみにここでindxは0..<3が、lengには3が返るけど、例えば
    let cnt = MemoryLayout.size(ofValue: vec0)
とするとcntには32が返る。要素の数が3の場合、キリのいいバイト境界にアラインされるようである。

もちろんこれを一気に大量にやらないとベクトルユニットのパフォーマンスを引き出すことはできない。ここまではStandard Libraryの範囲だけど、
import simd
とすると、要素が2、3、4のベクトルと、同じように$4 \times 4$までの行列と、ベクトル行列演算らしい演算が使えるようになる。たとえば
public typealias double2 = SIMD2
public typealias double2x2 = simd_double2x2
public typealias double2x3 = simd_double2x3
public typealias double2x4 = simd_double2x4
などとなっている。このsimd_で始まる型はsimd.typesで定義されている行列の型である。行列はcolumn majorに要素が並ぶそうである。つまりLAPACKと同じ、というかFORTRAN型である。

ベクトル行列らしい演算とは、例えば
public func distance(_ x: float2, _ y: float2) -> Float
public func distance_squared(_ x: float2, _ y: float2) -> Float
public func normalize(_ x: float2) -> float2
public func dot(_ x: float2, _ y: float2) -> Float
public func recip(_ x: float2) -> float2
public func rsqrt(_ x: float2) -> float2

public func matrix_determinant(_ x: simd_float2x2) -> Float
public func matrix_invert(_ x: simd_float2x2) -> simd_float2x2
public func matrix_transpose(_ x: simd_float2x2) -> float2x2

public static func * (lhs: simd_float2x2, rhs: float2) -> float2
public static func * (lhs: float2, rhs: simd_float2x2) -> float2
public static func * (lhs: simd_float2x2, rhs: float2x2) -> float2x2

public func reflect(_ x: float2, n: float2) -> float2
public func refract(_ x: float2, n: float2, eta: Float) -> float2
などである。上げたのは要素が2個のものだけど、同じように3、4個用がある(引数で区別して関数の名前は同じ)。それぞれ名前を見ればだいたいわかる。

reflect(_:n:)とrefract(_:n:eta:)はちょっと珍しいけど、光線の反射と屈折を計算する関数である。第1引数が入射光線を表すベクトルで、nは境界面の法線ベクトル。reflact(_:n:)は面で反射した光線になる。同じようにrefract(_:n:eta:)はetaが屈折率比として、屈折後の光線を返す。これ、なんでoptional(Nullable)でないんだろう。全反射の場合は何を返すんだ?

さらにクォータニオン
public struct simd_quatf {
    public var vector: simd_float4
    public init()
    public init(vector: simd_float4)
}

extension simd_quatf {
    public var real: Float
    public var imag: float3
    public var angle: Float { get }
    public var axis: float3 { get }
    public var conjugate: simd_quatf { get }
    public var inverse: simd_quatf { get }
    public var normalized: simd_quatf { get }
    public var length: Float { get }
    public func act(_ vector: float3) -> float3
    ....
}
などがDoubleと合わせて定義されている。ちなみに最後のact(_:)が引数ベクトルを実際に回転させる関数である。要素が4までなのは、このクォータニオンと同次座標をサポートするためだろうと思われるけど、同次座標として操作する関数は用意されていなくて、自分で書かないといけない。

simdをimportしたときに使えるようになる関数群は、実はCで
/usr/include/simd/simd.h
をインクルードしたときの古いCライブラリにある「simd_」という接頭辞を除いたものと同じである。Swiftは名前空間をサポートしているので同じ名前の関数があったらsimd.reflectとかで解決できるから、ということだろう。FoundationをimportしてObjective-C互換のクラスを参照するときNSがなくなるのと同じ理由だろう。ただし接頭辞が残っているのもある。このへんどういう違いなのかよくわからない。

reflect(_:n:)やrefract(_:n:eta:)なんかの存在理由は、昔からsimd.hにあったからというのが1番の理由だろう。3Dモデルを光線追跡でレンダリングするときにこれらを使うとは思えないんだけど、それともMetalを使うとGPUに展開してくれるから、とかいうのがあるのだろうか。なら嬉しいけど、ちょっとそれは厳しいか。勉強がひと段落したら、自分で書くのとSIMDを使うのとでどのくらいパフォーマンスに差があるか確認したい。多分それほど大きな差ではないとは思うんだけど、具体的に比較してみたい。

そしてもっと大きなベクトルや行列にはAccelerate frameworkにある古いBLASやLAPACKSwiftインターフェイスがあるけど、UnsafePointerだらけなのでCから呼ぶのに比べてどのくらい簡単になってるかはよくわからない)を、大きくてsparseな行列には新しいSparse Solverを使え、ということらしい。



昔のAltiVecではベタにベクトルを単純な構造体に書いて計算を定義したのとSIMDを直接呼ぶのとで効率がそこそこ違うということがあった。しかしSwiftで書いてるときにSIMDをわざわざ使うか、というと普通はあまりなさそうに思える。LLVMでは普通の配列の計算をベクトルユニットを使うように勝手に最適化してくれる場合もあるみたいだし。

ということは、最新の機能のどこかに使うつもりなのか、あるいはあっという間にobsoleteになるか、のどっちかだろう。ずっと昔期待していたAccelerateのLinear Algebraはドキュメントが整備されないまま、澱のようにFrameworkのディレクトリに今でも残ってるし。Metalに使われているのでそれとの整合性を取るのが1番の目的のような気がする。まあ生き残ってくれるなら、自分で勝手な構造体を定義するよりSIMDを使う方が汎用性はあるだろう。

OpenCLは結局僕には難しすぎてパフォーマンスが出せなかったので、Metalで再挑戦したいんだけど、全然勉強が進んでいない。Metalも難しい。
nice!(0)  コメント(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。