[MetalKit]34-Working-with-memory-in-Metal記憶體管理

蘋果API搬運工發表於2017-12-14

本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.

MetalKit系統文章目錄


今天我們關注一下使用GPU時的記憶體管理.Metal框架將記憶體資源定義為MTLBuffer物件,它是分配的無型別,無格式的記憶體(任何資料型別),MTLTexture物件則是分配的格式化記憶體來儲存圖片資料.我們在本文中只關注緩衝器.

建立MTLBuffer物件時有三種選項:

  • makeBuffer(length:options:) 建立一個MTLBuffer物件並分配一塊新記憶體.
  • makeBuffer(bytes:length:options:) 從一片已經存在的區域複製資料到一塊新分配的記憶體.
  • makeBuffer(bytesNoCopy:length:options:deallocator:) 重用一塊已經存在的記憶體.

讓我們建立一組緩衝器,看看資料是如何被傳遞到GPU的,及如何回傳給CPU.我們首先建立一塊緩衝器給輸入和輸出資料,並給它們初始化一些值:

let count = 1500
var myVector = [Float](repeating: 0, count: count)
var length = count * MemoryLayout< Float >.stride
var outBuffer = device.makeBuffer(bytes: myVector, length: length, options: [])
for (index, value) in myVector.enumerated() { myVector[index] = Float(index) }
var inBuffer = device.makeBuffer(bytes: myVector, length: length, options: [])
複製程式碼

新的MemoryLayout< Type >.stride語法在Swift 3被引入,來替代老的strideof(Type)函式.同時,因為記憶體排列的原因我們用.stride替代了.size.stride是指標增長時移動的位元組數.下一步是把我們緩衝器告訴命令編碼器:

encoder.setBuffer(inBuffer, offset: 0, at: 0)
encoder.setBuffer(outBuffer, offset: 0, at: 1)
複製程式碼

注意: <Metal最佳實踐指南>指出當我們的資料小於4KB(例如一個千位的浮點數)時就避免建立緩衝器.在本例中我們應該使用setBytes()函式來代替建立緩衝器.

最後一步是讀取GPU通過contents() 函式返回的資料,繫結記憶體資料到我們的輸出緩衝器上:

let result = outBuffer.contents().bindMemory(to: Float.self, capacity: count)
var data = [Float](repeating:0, count: count)
for i in 0 ..< count { data[i] = result[i] }
複製程式碼

Metal資源必須被配置好,以便快速記憶體訪問和驅動器效能優化.資源的儲存模式允許我們定義緩衝器和紋理的儲存位置和訪問許可權.如果你再看一眼上面我們建立緩衝器的地方,我們使用了預設([])的儲存模式.

所有的iOStvOS裝置支援unified memory model統一記憶體模型,它可以讓CPUGPU共享系統記憶體,而macOS裝置支援discrete memory model離散記憶體模型GPU擁有自己的記憶體.在iOStvOS中,Shared模式(MTLStorageModeShared)定義了系統記憶體可以被CPUGPU訪問,而Private模式(MTLStorageModePrivate)定義系統記憶體只能被GPU訪問.Shared模式是所有三種作業系統中的預設儲存模式.

ResourceManagement_iOStvOSMemory_2x.png

除了這兩種儲存模式外,macOS還有一種Managed模式(MTLStorageModeManaged),它為一種資源定義了一對同步記憶體,一個副本在系統記憶體中,另一個在視訊記憶體中來獲得更快的CPU和GPU本地訪問.

ResourceManagement_OSXMemory_2x.png

現在讓我們看看當我們將資料緩衝器傳送給GPU時,GPU上面發生了什麼.下面是個典型的頂點著色器例子:

vertex Vertices vertex_func(const device Vertices *vertices [[buffer(0)]], 
            		    constant Uniforms &uniforms [[buffer(1)]], 
            		    uint vid [[vertex_id]]) 
{
	...
}
複製程式碼

Metal Shading Language實現了地址空間修飾詞來指定當函式變數或引數分配時的記憶體區域:

  • device - 指緩衝器記憶體物件,從裝置記憶體池中分配,既可讀又可寫除非前面有const關鍵詞就是隻讀的.
  • constant - 指緩衝器記憶體物件,從裝置記憶體池中分配,但是是read-only只讀的.程式作用域內的變數必須被宣告為常量地址空間,並在宣告語句中被初始化.常量地址空間為多個例項在執行圖形或核心函式時訪問緩衝器中的同一塊位置的做了優化.
  • threadgroup - 僅用來分配核心函式中使用的變數,它們是為每個執行核心的執行緒組分配的,被執行緒組內的所有執行緒共享,只在執行核心的執行緒組的生命週期內才存在.
  • thread - 指每個執行緒的記憶體地址空間.分配在這個地址空間的變數對其它執行緒是不可見的.在圖形或核心函式中宣告的變數是分配線上程地址空間的.

作為獎勵,讓我們也看一下在Swift 3中另一種訪問記憶體位置的方法.這段程式碼是從前面的文章The Model I/O framework中摘抄的,所以我們就不再講解體素的細節了.只要想著我們需要遍歷一個陣列來獲取值:

let url = Bundle.main.url(forResource: "teapot", withExtension: "obj")
let asset = MDLAsset(url: url)
let voxelArray = MDLVoxelArray(asset: asset, divisions: 10, patchRadius: 0)
if let data = voxelArray.voxelIndices() {
    data.withUnsafeBytes { (voxels: UnsafePointer<MDLVoxelIndex>) -> Void in
        let count = data.count / MemoryLayout<MDLVoxelIndex>.size
        let position = voxelArray.spatialLocation(ofIndex: voxels.pointee)
        print(position)
    }
}
複製程式碼

在本例中,MDLVoxelArray物件有了個名為spatialLocation()的函式,它讓我們用一個MDLVoxelIndex型別的UnsafePointer指標來遍歷陣列,並通過每個位置的pointee來訪問資料.在本例中,我們只列印出地址中的第一個值,但一個簡單的迴圈可以讓我們得到所有的數,像這樣:

var voxelIndex = voxels
for _ in 0..<count {
    let position = voxelArray.spatialLocation(ofIndex: voxelIndex.pointee)
    print(position)
    voxelIndex = voxelIndex.successor()
}
複製程式碼

原始碼source code已釋出在Github上.

下次見!

相關文章