モノトーンの伝説日記

Apex Legends 楽しい!!

Swift で SIMD する。(SIMDKit も公開)

 こんにちはモノトーンです。

 今日は,Swift で SIMD 吐き出すための方法や,実際にどんなコードが吐き出されるか,そして最後には SIMDKit についてお話ししようと思います。

1. import simd で使える

 CGFloat 型は 32-bit デバイスでは Float (Float32),64-bit デバイスでは Double (Float64)。

 個人的には,32-bit デバイスの時点で,CGFloat は数値計算には使えませんので,正直,座標とか色とかに使うデータなら Float32 で統一して欲しいんですけどね……w 特に座標データに現時点で Float64 は別にいらないですし(まあ将来,32K ディスプレイとかになってきたら必要かもしれません)

 まあそれはさておき,ARMv7 から使える Neon 命令は 128-bit まで扱えます。しかも,新しめのプロセッサーだと 128-bit 一回で演算すると思います。古めだと,64-bit 2回かな。

 どちらにせよ,Intel 系プロセッサーのように 256-bit (AVX, AVX2) や 512-bit (AVX-512F) を演算することはできないですね。それでも,座標演算では,多少早くなることを期待できると思います。

 あと,macOS 向けだと,AVX2 完全サポートしてる機種だけが対応しているわけじゃないので,ガチでやるなら,SSE 版と AVX 版作って分岐してパフォーマンスが出る設計にしないといけないですが,いちいちプラットフォーム検出とかこの手のライブラリーに組み込むのはめんどくさいので,SSE で処理させちゃうので,実質 NEON と変わりないコードが生成されるかと。

 以下は単純なサンプルコードです。

import simd
#if os(macOS)
import struct CoreGraphics.CGGeometry.CGFloat
import struct CoreGraphics.CGGeometry.CGRect
#else
import struct CoreGraphics.CGFloat
import struct CoreGraphics.CGRect
#endif

extension CGRect {
   #if arch(arm64) || arch(x86_64)
    internal typealias SimdType = simd_double4
   #else
    internal typealias SimdType = simd_float4
   #endif
    
    internal init(simd: SimdType) {
        self.init(x: CGFloat(simd.x), y: CGFloat(simd.y), width: CGFloat(simd.z), height: CGFloat(simd.w))
    }
    
    internal var simd: SimdType {
        return SimdType(x: self.origin.x.native, y: self.origin.y.native, z: self.size.width.native, w: self.size.height.native)
    }
    
    public static func + (lhs: CGRect, rhs: CGRect) -> CGRect {
        return CGRect(simd: lhs.simd + rhs.simd)
    }
}

2. AArch64 の場合

 double4 を fadd で 2 回演算してます。ARM は MIPS とかと同じく 3op 命令ですね。

 dN は 64-bit レジスター (double word の頭文字 d),vN は 128-bit レジスター (vector の頭文字 v) です。

 vN のインデックス [0] は [63:0] に対応,[1] は [127:64] に対応しています。

    .globl _$SSo6CGRectV4mainE1poiyA2B_ABtFZ
    .p2align  2
_$SSo6CGRectV4mainE1poiyA2B_ABtFZ:
    mov.d  v0[1], v1[0] ; v0[127:64] ← v1[63:0] # Ap = (Ax, Ay)
    mov.d  v2[1], v3[0] ; v2[127:64] ← v3[63:0] # As = (Aw, Ah)
    mov.d  v4[1], v5[0] ; v4[127:64] ← v5[63:0] # Bp = (Bx, By)
    mov.d  v6[1], v7[0] ; v6[127:64] ← v7[63:0] # Bs = (Bw, Bh)
    fadd.2d  v2, v2, v6 ; v2 ← v2 + v6 # Cs = As + Bs
    fadd.2d  v0, v0, v4 ; v0 ← v0 + v4 # Cp = Ap + Bp
    mov  d1, v0[1] ; d1 ← v0[127:64] # Cy をコピー
    mov  d3, v2[1] ; d3 ← v2[127:64] # Ch をコピー
    ret

swiftc -emit-assembly -whole-module-optimization -Osize CGRect+Operators.swift -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.1.sdk -target arm64-apple-ios8.0 でコンパイル

入力

  • d0: Ax
  • d1: Ay
  • d2: Aw
  • d3: Ah
  • d4: Bx
  • d5: By
  • d6: Bw
  • d7: Bh

出力

  • d0: Cx
  • d1: Cy
  • d2: Cw
  • d3: Ch

3. AMD64 / Intel 64 の場合

 AT&T Syntax です。

 xmmN レジスターは 128-bit レジスターという意味です。

    .globl _$SSo6CGRectV4mainE1poiyA2B_ABtFZ
    .p2align  4, 0x90
_$SSo6CGRectV4mainE1poiyA2B_ABtFZ:
    pushq    %rbp
    movq %rsp, %rbp
    unpcklpd %xmm1, %xmm0 ; xmm0[127:0] ← (xmm1[63:0] << 64) | xmm0[63:0] # Ap = (Ax, Ay)
    unpcklpd %xmm3, %xmm2 ; xmm2[127:0] ← (xmm3[63:0] << 64) | xmm2[63:0] # As = (Aw, Ah)
    unpcklpd %xmm5, %xmm4 ; xmm4[127:0] ← (xmm5[63:0] << 64) | xmm4[63:0] # Bp = (Bx, By)
    addpd    %xmm0, %xmm4 ; xmm4 ← xmm0 + xmm4 # Cp = Ap + Bp
    unpcklpd %xmm7, %xmm6 ; xmm6[127:0] ← (xmm7[63:0] << 64) | xmm6[63:0] # Bs = (Bw, Bh)
    addpd    %xmm2, %xmm6 ; xmm6 ← xmm2 + xmm6 # Cs = As + Bs
    movapd   %xmm4, %xmm1 ; xmm1 ← xmm4 # Cx
    movhlps  %xmm1, %xmm1 ; xmm1 ← (xmm1[127:64] << 64) | xmm1[63:0] # Cy (下位 64-bit のみが使われる)
    movapd   %xmm6, %xmm3 ; xmm3 ← xmm6 # Cw
    movhlps  %xmm3, %xmm3 ; xmm3 ← (xmm3[127:64] << 64) | xmm3[63:0] # Ch (下位 64-bit のみが使われる)
    movapd   %xmm4, %xmm0 ; xmm0 ← xmm4 # Cx をコピー
    movapd   %xmm6, %xmm2 ; xmm2 ← xmm6 # Cw をコピー
    popq %rbp
    retq

swiftc -emit-assembly -whole-module-optimization -Osize CGRect+Operators.swift でコンパイル

入力

  • xmm0: Ax
  • xmm1: Ay
  • xmm2: Aw
  • xmm3: Ah
  • xmm4: Bx
  • xmm5: By
  • xmm6: Bw
  • xmm7: Bh

出力

  • xmm0: Cx
  • xmm1: Cy
  • xmm2: Cw
  • xmm3: Ch

4. SIMDKit について

 CGPoint, CGSize, CGVector, UIEdgeInsets/NSEdgeInsets, NSDirectionalEdgeInsets (macOS 除く) の演算関連を定義しています。

github.com

4.1 CGPoint, CGSize, CGVector 系 float2/double2

  • Functions
    • abs
    • ceil, floor, round (no SIMD), trunc
    • clamp
    • length, length_squared
    • max, min, maxmin (x, width, dx は max, y, height, dy は min), minmax
    • reduce_max, reduce_min
  • Operators (V は Vector, C は Const.)
    • -V
    • V + V, C + V, V + C
    • V - V, C - V, V - C
    • V +- V, C +- V, V +- C (no SIMD)
    • V -+ V, C -+ V, V -+ C (no SIMD)
    • V * V, C * V, V * C
    • V / V, C / V, V / C
    • V += V, V += C
    • V -= V, V -= C
    • V +-= V, V +-= C
    • V -+= V, V -+= C
    • V = V, V = C
    • V /= V, V /= C
  • Mutatings
    • add(x:), add(y:), add(x:y:), add(offset:)
  • Nonmutatings
    • adding(x:), adding(y:), adding(x:y:), adding(offset:)
    • setting(x:), setting(y:)

4.2 CGRect 系 float4/double4

  • Functions
    • abs
    • ceil, floor, round (no SIMD), trunc
    • clamp
    • max, min
    • reduce_max, reduce_min
  • Operators (V は Vector, C は Const.)
    • -V
    • V + V, C + V, V + C
    • V - V, C - V, V - C
    • V * V, C * V, V * C
    • V / V, C / V, V / C
    • V += V, V += C
    • V -= V, V -= C
    • V = V, V = C
    • V /= V, V /= C
  • Mutatings
    • add(x:), add(y:), add(x:y:), add(offset:)
    • add(width:), add(height:), add(width:height:), add(length:), add(size:)
  • Nonmutatings
    • adding(x:), adding(y:), adding(x:y:), adding(offset:)
    • adding(width:), adding(height:), adding(width:height:), adding(length:), adding(size:)
    • setting(x:), setting(y:), setting(x:y:), setting(origin:)
    • setting(width:), setting(height:), setting(width:height:), setting(length:), setting(size:)

4.3 UIEdgeInsets, NSEdgeInsets 系 float4/double4

  • Functions
    • abs
    • ceil, floor, round (no SIMD), trunc
    • clamp
    • max, min
    • reduce_max, reduce_min
  • Operators (V は Vector, C は Const.)
    • -V
    • V + V, C + V, V + C
    • V - V, C - V, V - C
    • V * V, C * V, V * C
    • V / V, C / V, V / C
    • V += V, V += C
    • V -= V, V -= C
    • V = V, V = C
    • V /= V, V /= C
  • Mutatings
    • add(top:), add(left:), add(bottom:), add(right:), add(vertical:), add(horizontal:), add(all:)
  • Nonmutatings
    • adding(top:), adding(left:), adding(bottom:), adding(right:), adding(vertical:), adding(horizontal:), adding(all:)
    • setting(top:), setting(left:), setting(bottom:), setting(right:), setting(vertical:), setting(horizontal:)

4.4 NSDirectionalEdgeInsets 系 float4/double4

  • Functions
    • abs
    • ceil, floor, round (no SIMD), trunc
    • clamp
    • max, min
    • reduce_max, reduce_min
  • Operators (V は Vector, C は Const.)
    • -V
    • V + V, C + V, V + C
    • V - V, C - V, V - C
    • V * V, C * V, V * C
    • V / V, C / V, V / C
    • V += V, V += C
    • V -= V, V -= C
    • V = V, V = C
    • V /= V, V /= C
  • Mutatings
    • add(top:), add(leading:), add(bottom:), add(trailing:), add(vertical:), add(horizontal:), add(all:)
  • Nonmutatings
    • adding(top:), adding(leading:), adding(bottom:), adding(trailing:), adding(vertical:), adding(horizontal:), adding(all:)
    • setting(top:), setting(leading:), setting(bottom:), setting(trailing:), setting(vertical:), setting(horizontal:)

まとめ

 初めて Xcode でマルチプラットフォームプロジェクトを作ってみたんですけど,結構簡単に作れますね。あと,Carthage も tag さえつければ特に何もしなくていいのもすごくいいですねー。

 これのサブセット版を今の開発アプリで作り始めて,それが手を広げすぎて,こんなことになっちゃいましたがw

 ま,これだけ用意しておけば,色々困ることもないでしょう。

 個人的に,上位から順番に反映させていく,UIEdgeInsets の setting(horizontal:) とか結構便利だなーって思ってますね。layoutMargins.setting(horizontal: 10.0) みたいに,左右の余白を 10.0 にして,上下はそのまま引き継ぐ,みたいな記述がとても楽なので。

 ま,名前に SIMD って入れたんですけど,あんまりいいのが思いつかなかったのと,おそらく誰も使わないんで,テキトーにって感じですかねw

 不具合あったら GitHub の Issue に投げてくださいー。