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には
SIMD protocolには基本的な
SIMDのextensionにSIMDScalarを限定していろいろな演算子が定義されてるけど、何をする演算なのかドキュメントには何も書いてないのがたくさんある。
具体的にどう使うかと言うと
もちろんこれを一気に大量にやらないとベクトルユニットのパフォーマンスを引き出すことはできない。ここまではStandard Libraryの範囲だけど、
ベクトル行列らしい演算とは、例えば
reflect(_:n:)とrefract(_:n:eta:)はちょっと珍しいけど、光線の反射と屈折を計算する関数である。第1引数が入射光線を表すベクトルで、nは境界面の法線ベクトル。reflact(_:n:)は面で反射した光線になる。同じようにrefract(_:n:eta:)はetaが屈折率比として、屈折後の光線を返す。これ、なんでoptional(Nullable)でないんだろう。全反射の場合は何を返すんだ?
さらにクォータニオン
simdをimportしたときに使えるようになる関数群は、実はCで
reflect(_:n:)やrefract(_:n:eta:)なんかの存在理由は、昔からsimd.hにあったからというのが1番の理由だろう。3Dモデルを光線追跡でレンダリングするときにこれらを使うとは思えないんだけど、それともMetalを使うとGPUに展開してくれるから、とかいうのがあるのだろうか。なら嬉しいけど、ちょっとそれは厳しいか。勉強がひと段落したら、自分で書くのとSIMDを使うのとでどのくらいパフォーマンスに差があるか確認したい。多分それほど大きな差ではないとは思うんだけど、具体的に比較してみたい。
そしてもっと大きなベクトルや行列にはAccelerate frameworkにある古いBLASやLAPACK(Swiftインターフェイスがあるけど、UnsafePointerだらけなのでCから呼ぶのに比べてどのくらい簡単になってるかはよくわからない)を、大きくてsparseな行列には新しいSparse Solverを使え、ということらしい。
昔のAltiVecではベタにベクトルを単純な構造体に書いて計算を定義したのとSIMDを直接呼ぶのとで効率がそこそこ違うということがあった。しかしSwiftで書いてるときにSIMDをわざわざ使うか、というと普通はあまりなさそうに思える。LLVMでは普通の配列の計算をベクトルユニットを使うように勝手に最適化してくれる場合もあるみたいだし。
ということは、最新の機能のどこかに使うつもりなのか、あるいはあっという間にobsoleteになるか、のどっちかだろう。ずっと昔期待していたAccelerateのLinear Algebraはドキュメントが整備されないまま、澱のようにFrameworkのディレクトリに今でも残ってるし。Metalに使われているのでそれとの整合性を取るのが1番の目的のような気がする。まあ生き残ってくれるなら、自分で勝手な構造体を定義するよりSIMDを使う方が汎用性はあるだろう。
OpenCLは結局僕には難しすぎてパフォーマンスが出せなかったので、Metalで再挑戦したいんだけど、全然勉強が進んでいない。Metalも難しい。
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◎というのが定義されている。◎は要素の数で2、3、4、8、16、32、64がある。つまりSIMD4というと要素の型がScalarで要素が4つの構造体だということになる。SIMDScalarはassociatedtypeが定義されるだけの親のないprotocolだけど、普通のIntやFloatやDoubleがSIMDScalarをconformしてることになってる。: SIMD where Scalar : SIMDScalar
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などとできる。乱数発生が並列的に行われるかどうか、はよくわからないが、単純な擬似乱数ならベクトルユニットを使うことができるだろう。そしてvec3の計算には自動的にCPUのベクトルユニットが使われるはずである(確認してないので知らんけど)。vec0には[0,1]の乱数が要素の3次元ベクトルが入る。ちなみにここでindxは0..<3が、lengには3が返るけど、例えば(arrayLiteral: 1.0, 2.0, 3.0) let vec3 = vec1 - vec0
let cnt = MemoryLayout.size(ofValue: vec0)とするとcntには32が返る。要素の数が3の場合、キリのいいバイト境界にアラインされるようである。
もちろんこれを一気に大量にやらないとベクトルユニットのパフォーマンスを引き出すことはできない。ここまではStandard Libraryの範囲だけど、
import simdとすると、要素が2、3、4のベクトルと、同じように$4 \times 4$までの行列と、ベクトル行列演算らしい演算が使えるようになる。たとえば
public typealias double2 = SIMD2などとなっている。このsimd_で始まる型はsimd.typesで定義されている行列の型である。行列はcolumn majorに要素が並ぶそうである。つまりLAPACKと同じ、というかFORTRAN型である。public typealias double2x2 = simd_double2x2 public typealias double2x3 = simd_double2x3 public typealias double2x4 = simd_double2x4
ベクトル行列らしい演算とは、例えば
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やLAPACK(Swiftインターフェイスがあるけど、UnsafePointerだらけなのでCから呼ぶのに比べてどのくらい簡単になってるかはよくわからない)を、大きくてsparseな行列には新しいSparse Solverを使え、ということらしい。
昔のAltiVecではベタにベクトルを単純な構造体に書いて計算を定義したのとSIMDを直接呼ぶのとで効率がそこそこ違うということがあった。しかしSwiftで書いてるときにSIMDをわざわざ使うか、というと普通はあまりなさそうに思える。LLVMでは普通の配列の計算をベクトルユニットを使うように勝手に最適化してくれる場合もあるみたいだし。
ということは、最新の機能のどこかに使うつもりなのか、あるいはあっという間にobsoleteになるか、のどっちかだろう。ずっと昔期待していたAccelerateのLinear Algebraはドキュメントが整備されないまま、澱のようにFrameworkのディレクトリに今でも残ってるし。Metalに使われているのでそれとの整合性を取るのが1番の目的のような気がする。まあ生き残ってくれるなら、自分で勝手な構造体を定義するよりSIMDを使う方が汎用性はあるだろう。
OpenCLは結局僕には難しすぎてパフォーマンスが出せなかったので、Metalで再挑戦したいんだけど、全然勉強が進んでいない。Metalも難しい。
2019-05-10 21:31
nice!(0)
コメント(0)
コメント 0