[ARKit]9-3D/AR 中的 simd 型別

蘋果API搬運工發表於2018-06-21

說明

ARKit系列文章目錄

simd

SIMD全稱Single Instruction Multiple Data,單指令多資料流,能夠複製多個運算元,讀作[洗目~底]

以加法指令為例,單指令單資料(SISD)的CPU對加法指令譯碼後,執行部件先訪問記憶體,取得第一個運算元;之後再一次訪問記憶體,取得第二個運算元;隨後才能進行求和運算。而在SIMD型的CPU中,指令譯碼後幾個執行部件同時訪問記憶體,一次性獲得所有運算元進行運算。這個特點使SIMD特別適合於多媒體應用等資料密集型運算。

iOS中的simd

iOS11開始,正式引入這個運算,用於加速圖形圖象的處理.

SceneKit中保留舊屬性名稱和型別的同時,在iOS11新增新型別,並以simd_字首開頭比如SCNNode型別的屬性中:

// 舊的變換矩陣
open var transform: SCNMatrix4

// 新的simd型別矩陣
@available(iOS 11.0, *)
open var simdTransform: simd_float4x4

複製程式碼

SceneKit中這兩者只要改變一個,另一個也會隨之改變,作用是一樣的,只是格式不同.

ARKit在iOS11才推出,直接使用了新型別,名稱前也沒有字首,比如在ARAnchor型別的屬性中:

/**
     The transformation matrix that defines the anchor’s rotation, translation and scale in world coordinates.
     */
open var transform: matrix_float4x4 { get }
複製程式碼

下面我們來仔細看看matrix_float4x4這個結構體,只是個別名,真實型別還是simd_float4x4:

public typealias matrix_float4x4 = simd_float4x4
複製程式碼
/*! @abstract A matrix with 4 rows and 4 columns.                             */
public struct simd_float4x4 {

    public var columns: (simd_float4, simd_float4, simd_float4, simd_float4)

    public init()

    public init(columns: (simd_float4, simd_float4, simd_float4, simd_float4))
}
複製程式碼

裡面是4個simd_float4向量型別組成的,而這只是個別名,真正型別是float4,它也是結構體:

/*! @abstract A vector of four 32-bit floating-point numbers.
 *  @description In C++ and Metal, this type is also available as
 *  simd::float4. The alignment of this type is greater than the alignment
 *  of float; if you need to operate on data buffers that may not be
 *  suitably aligned, you should access them using simd_packed_float4
 *  instead.
該向量,由4個32-bit浮點陣列成.在C++和Metal中,該型別為simd::float4.該型別的對齊大於float型別的對齊;如果你需要直接運算元據緩衝可能會有對齊問題,你應該通過simd_packed_float4來訪問.
*/
public typealias simd_float4 = float4


/// A vector of four `Float`.  This corresponds to the C and
/// Obj-C type `vector_float4` and the C++ type `simd::float4`.
/// 四個`Float`組成的向量.在C和ObjC中的對應型別為`vector_float4`,在C++中為`simd::float4`.
public struct float4 {

    public var x: Float

    public var y: Float

    public var z: Float

    public var w: Float

    /// Initialize to the zero vector.
    public init()

    /// Initialize a vector with the specified elements.
    public init(_ x: Float, _ y: Float, _ z: Float, _ w: Float)

    /// Initialize a vector with the specified elements.
    public init(x: Float, y: Float, z: Float, w: Float)

    /// Initialize to a vector with all elements equal to `scalar`.
    public init(_ scalar: Float)

    /// Initialize to a vector with elements taken from `array`.
    ///
    /// - Precondition: `array` must have exactly four elements.
    public init(_ array: [Float])

    /// Access individual elements of the vector via subscript.
    public subscript(index: Int) -> Float
}
複製程式碼

simd_floatN & floatN & simd_packed_floatN

float4為例,總共有多種型別和它相關:

public typealias simd_float4 = float4

public typealias vector_float4 = simd_float4

public typealias simd_packed_float4 = float4

還有一個是已經廢除的packed_float4:

public typealias packed_float4 = simd_packed_float4

Xcode文件對此進行了解釋:

這些型別是基於clang特性,叫做"擴充套件向量型別"或"OpenCL向量型別"(可用在C,Objective-C和C++中).還有一些新特性讓它比傳統的simd型別更方便使用:

  • 重寫了基礎運算子,可以進行向量-向量運算,向量-標量運算.
  • 可以通過"."操作符及子元素名稱"x", "y", "z", "w",像陣列那樣訪問子元素.
  • 還有一些已命名的子向量:.lo和.hi分別是向量的前半部分和後半部分,.even和.odd則分別是偶數位和奇數位向量元素.
  • Clang提供了一些有用的內建操作可以操作這些向量型別:__builtin_shufflevector__builtin_convertvector.
  • <simd/simd.h>標頭檔案中定義了大量的向量和矩陣操作,適用於這些型別.
  • 你也可以根據不同架構,引用<immintrin.h> and <arm_neon.h>來使用simd型別.

simd向量的型別定義有:

 simd_charN   where N is 1, 2, 3, 4, 8, 16, 32, or 64.
 simd_ucharN  where N is 1, 2, 3, 4, 8, 16, 32, or 64.
 simd_shortN  where N is 1, 2, 3, 4, 8, 16, or 32.
 simd_ushortN where N is 1, 2, 3, 4, 8, 16, or 32.
 simd_intN    where N is 1, 2, 3, 4, 8, or 16.
 simd_uintN   where N is 1, 2, 3, 4, 8, or 16.
 simd_floatN  where N is 1, 2, 3, 4, 8, or 16.
 simd_longN   where N is 1, 2, 3, 4, or 8.
 simd_ulongN  where N is 1, 2, 3, 4, or 8.
 simd_doubleN where N is 1, 2, 3, 4, or 8.
複製程式碼

這些型別相比底層的標量型別有更大的對齊間隔;它們對齊的是:向量[1]的尺寸與目標平臺Clang中"最大對齊間隔"[2],這兩者中的最小值.

[1]注意,3維向量和4維向量尺寸是一樣的,因為3維有一個隱藏的通道.

[2]一般來說,架構層面的向量寬度是16, 32, or 64 bytes.如果你需要精確控制對齊方式,需要小心一點,因為這個值會根據編譯設定的不同而變化.

對於simd_typeN型別來說,除了N等於1或3外,都有相應型別的simd_packed_typeN,它只要求對齊方式匹配底層標量型別.如果你需要處理指向標量的指標或包含標量的陣列,就使用simd_packed_typeN型別:

//float *pointerToFourFloats是指向四維Float陣列的指標
void myFunction(float *pointerToFourFloats) {
   
    // 這樣做是個bug,因為`pointerToFourFloats`並不滿足`simd_float4`型別要求的對齊方式;強制轉換很可能在執行時導致崩潰
    simd_float4 *vecptr = (simd_float4 *)pointerToFourFloats;

  
    // 應該轉換為`simd_packed_float4`型別:
    simd_packed_float4 *vecptr = (simd_packed_float4 *)pointerToFourFloats;
  
    // `simd_packed_float4`型別的對齊方式和`float`相同,所以這個轉換是安全的,可以讓我們成功載入一個向量.
    // `simd_packed_float4`型別可以不用轉換就直接賦值給`simd_float4`;它們的型別只有在作為指標或陣列時才會有所不同.
    simd_float4 vector = vecptr[0];
}
 
 
複製程式碼

所有simd_開頭的型別,在C++中都在simd::名稱空間中;比如,simd_char4可以用simd::char4. 這些型別大部分都和Metal shader language中的向量型別相匹配,除了那些超過4維的向量,因為Metal中沒有超過4的向量.

型別的轉化

swift中的轉換

在swift中沒有指標的轉換問題,而simd_floatN & vector_floatN & simd_packed_floatN這些型別本質上都是floatN型別,所以轉換就相當簡單了,直接init()重新構造一個就可以了:

let floats:[Float] = [1,2,3,4]
// 直接轉換為simd_float4型別,本質是呼叫了float4.init()方法,該init()方法可以接收多種型別,其中就包括了[Float]型別引數
let testVect4:simd_float4 = simd_float4(floats)
// 轉換為simd_packed_float4型別,再賦值給simd_float4型別,本質也是呼叫了float4.init()方法
let result2Vect4:simd_float4 = simd_packed_float4(floats)

// 先轉為simd_packed_float4型別,原理也是init()方法,不使用as再次轉換也可以
let packedVect4:simd_packed_float4 = simd_packed_float4(floats)
let resultVect4:simd_float4 = packedVect4 as simd_float4

// 直接強制轉換是不可以的,比如下面的方法
let result1111Vect4:simd_float4 = (simd_float4)floats
let result2222Vect4:simd_float4 = floats as! simd_float4
複製程式碼

反過來轉換也是一樣的道理,呼叫init()方法

let simdVect4 = simd_float4(1,2,3,4)
// 呼叫陣列的Array.init()方法
let array:[Float] = Array(simdVect4);
複製程式碼

在測試中發現,swift中的[Float]型別和simd_float4型別即float4在記憶體中儲存結構完全不同:

[ARKit]9-3D/AR 中的 simd 型別
[ARKit]9-3D/AR 中的 simd 型別

OC中的轉換

OC中因為有指標的概念,所以需要注意的是指標的型別.

在OC和C中,直接轉換指標型別,資料的結構並沒有變,而結構體型別在經過一次賦值後,會發生複製,在複製過程中型別轉換就完成了:

float floats[4] = {1,2,3,4};
// 此時packedVect4指標仍然指向的是陣列開頭,只是指標的型別不同
simd_packed_float4 *packedVect4 = (simd_packed_float4 *)floats;
// *packedVect4表示取出packedVect4指標指向的資料,然後賦值給resultVect4,C語言中常量和結構體的賦值是值傳遞,所謂值傳遞就是複製一份出來,在複製並賦值的同時,型別轉換就完成了.
simd_float4 resultVect4 = *packedVect4;


// 因為simd_packed_float4型別和simd_float4本質都是float4型別,理論上用哪個都無所謂,我測試的過程中未發現異常.
// 但是蘋果給出了示例程式碼中使用了simd_packed_float4型別做中轉,而且特別說明:它們的型別只有在作為指標或陣列時才會有所不同.所以在OC中還是用simd_packed_float4中轉一下更安全.
simd_float4 result2Vect4 = *(simd_packed_float4 *)floats;
simd_float4 result3Vect4 = *(simd_float4*)floats;
複製程式碼

相反的轉換,因為C語言中的陣列初始化只能用{},所以限制了型別轉換,非常不便:

simd_float4 simdVect4 = simd_make_float4(1, 2, 3, 4);
// 只能一個個數值取出,再初始化C陣列
float arr[4] = {simdVect4.x,simdVect4.y,simdVect4.z,simdVect4.w};
float arr2[4] = {simdVect4[0],simdVect4[1],simdVect4[2],simdVect4[3]};
複製程式碼

測試中發現,不考慮記憶體對齊,OC中的float [4]型別陣列與simd_float4型別在記憶體中儲存結構是一樣的:

[ARKit]9-3D/AR 中的 simd 型別
[ARKit]9-3D/AR 中的 simd 型別

使用中的優勢

在WWDC2018中,也講到了simd型別的應用. 主要有幾點:

  • 用在2,3,4維矩陣中,和2, 3, 4, 8, 16, 32, 或 64維向量中.
  • 向量與向量,向量與標題可以用運算子(+,-,*,/)進行運算.
  • 常見的向量和幾何運算(dot, length, clamp).
  • 支援超越函式(如sin,cos).
  • 四元數

舉例,以前的做法,常規陣列很慢,simd很快:

// 求兩個向量的平均值,以前做法
var x:[Float] = [1,2,3,4]
var y:[Float] = [3,3,3,3]
var z = [Float](repeating:0, count:4)
for i in 0..<4 {
    z[i] = (x[i] + y[i]) / 2.0
}



// 現在做法
let x = simd_float4(1,2,3,4)
 let y = simd_float4(3,3,3,3)
let z = 0.5 * (x + y)
複製程式碼

重點講一下四元數在旋轉中的使用.

// 要旋轉的向量(下圖中紅點)
 let original = simd_float3(0,0,1)
  // 旋轉軸和角度
 let quaternion = simd_quatf(angle: .pi / -3,
 axis: simd_float3(1,0,0))
 // 應用旋轉(下圖中黃點)
  let rotatedVector = simd_act(quaternion, original)
複製程式碼

[ARKit]9-3D/AR 中的 simd 型別

開發中,我們不會只繞一個軸旋轉,可能會是兩個或或複雜.

// 要旋轉的向量(紅點)
let original = simd_float3(0,0,1)
// 旋轉軸
let quaternion = simd_quatf(angle: .pi / -3,
                            axis: simd_float3(1,0,0))
let quaternion2 = simd_quatf(angle: .pi / 3,
                             axis: simd_float3(0,1,0))
// 組合兩個旋轉
let quaternion3 = quaternion2 * quaternion
// 應用旋轉(黃點)
let rotatedVector = simd_act(quaternion3, original)
複製程式碼

[ARKit]9-3D/AR 中的 simd 型別

另外simd還支援四元數的插值運算,Slerp Interpolation(球面線性插值)和Spline Interpolation(樣條曲線插值).

// Slerp Interpolation球面線性插值
let blue = simd_quatf(...) //藍色
let green = simd_quatf(...) //綠色
let red = simd_quatf(...) //紅包

for t: Float in stride(from: 0, to: 1, by: 0.001) {
    let q = simd_slerp(blue, green, t) //從藍色到綠色的插值曲線(最短球面距離)
    // Code to Add Line Segment at `q.act(original)`
}

for t: Float in stride(from: 0, to: 1, by: 0.001) {
    let q = simd_slerp_longest(green, red, t) //從藍色到綠色的插值曲線(最長球面距離)
    // Code to Add Line Segment at `q.act(original)`
}
複製程式碼

[ARKit]9-3D/AR 中的 simd 型別

// Spline Interpolation樣條曲線插值
let original = simd_float3(0,0,1)
let rotations: [simd_quatf] = ...
for i in 1 ... rotations.count - 3 {
    for t: Float in stride(from: 0, to: 1, by: 0.001) {
        let q = simd_spline(rotations[i - 1],
                            rotations[i],
                            rotations[i + 1],
                            rotations[i + 2],
                            t)
    }
    // Code to Add Line Segment at `q.act(original)`
}
複製程式碼

[ARKit]9-3D/AR 中的 simd 型別

兩者在旋轉運動中的區別還是很明顯的,如下圖頂點軌跡

[ARKit]9-3D/AR 中的 simd 型別

相關文章