Metal新特性:大幅度提升iOS端性能

作爲較早在客戶端側選擇Flutter方案的技術團隊,性能和用戶體驗一直是閒魚技術團隊在開發中比較關注的點。而Metal這樣的直接操作GPU的底層接口無疑會給閒魚技術團隊突破性能瓶頸提供一些新的思路。
本文將會詳細闡述一下這次大會Metal相關的新特性,以及對於閒魚技術和整個淘系技術來說,這些新特性帶來了哪些技術啓發與思考。

前言

Metal 是一個和 OpenGL ES 類似的面向底層的圖形編程接口,通過使用相關的 api 可以直接操作 GPU ,最早在 2014 年的 WWDC 的時候發佈。Metal 是 iOS 平臺獨有的,意味着它不能像 OpenGL ES 那樣支持跨平臺,但是它能最大的挖掘蘋果移動設備的 GPU 能力,進行復雜的運算,像 Unity 等遊戲引擎都通過 Metal 對 3D 能力進行了優化, App Store 還有相應的運用 Metal 技術的遊戲專題。

閒魚團隊是比較早在客戶端側選擇Flutter方案的技術團隊,當前的閒魚工程裏也是一個較爲複雜的Native-Flutter混合工程。作爲一個2C的應用,性能和用戶體驗一直是閒魚技術團隊在開發中比較關注的點。而Metal這樣的直接操作GPU的底層接口無疑會給閒魚技術團隊突破性能瓶頸提供一些新的思路。

下面會詳細闡述一下這次大會Metal相關的新特性,以及對於閒魚技術和整個淘系技術來說,這些新特性帶來了哪些技術啓發與思考。

Metal相關新特性

Harness Apple GPUs with Metal

這一章其實主要介紹的是Apple GPU的在圖形渲染上的原理和工作流,是一些比較底層的硬件原理。當我們使用Metal進行App或者是遊戲的構建的時候,Metal會利用GPU的tile-based deferred rendering (TBDR)架構給應用和遊戲帶來非常可觀的性能提升。這一章主要就是介紹GPU的的架構和能力,以及TBDR架構進行圖像渲染的原理和流程。總之就是號召開發者們使用Metal來構建應用和遊戲。因爲這個session沒有涉及到上層的軟件開發,就不對視頻的具體內容進行贅述了。詳情可見:Harness Apple GPUs with Metal

Optimize Metal apps and games with GPU counters

這一章主要介紹了Xcode中的GPU性能分析工具Instrument,這個工具現在已經支持了GPU的性能分析。然後從多個方面分析了GPU的性能瓶頸,以及性能瓶頸出現時的優化點。總體來說就是通過性能分析工具來優化我們的App或者遊戲,讓整個畫面更加流暢。整個章節主要分爲五個部分:

✎ 總體介紹

這個環節主要是快速回顧了一下Apple的GPU的架構和渲染流程。然後因爲很多渲染任務都需要在不同的硬件單元上進行,例如ALU和TPU。他們對不同的吞吐量有着不同的度量。有很多GPU的性能指標需要被考慮,所以推出了GPU性能計數器。這個計數器可能測量到GPU的利用率,過高和過低都會造成我們的渲染性能瓶頸。關於計數器的具體使用,參考官方的video效果會更好:Optimize Metal apps and games with GPU counters(6:37~9:57),主要使用了Instrument工具,關於工具的全面詳細的使用可以參考WWDC19的session videoGetting Started with Instruments

✎ 性能瓶頸分析

這一章主要介紹了造成GPU性能瓶頸的各個方面以及它們的優化點。主要分爲六個方面,如下圖所示:

1.Arithmetic(運算能力)

GPU中通常通過ALU(Arithmetic Logic Unit)來處理各種運算,例如位操作,關係操作等。他是着色器核心的一部分。在這裏一些複雜的操作或者是高精度的浮點運算都會造成一些性能瓶頸,所以給出以下建議來進行優化:

如上圖所示,我們可以使用近似或者是查找表的方式來替換複雜的運算。此外,我們可以將全精度的浮點數替換爲半精度的浮點數。儘量避免隱式轉換,避免32位浮點數的輸入。以及確保所有的着色器都使用Metal的“-ffast-math”來進行編譯。

2.Texture Read and Write

GPU通過Texture Processing Unit來處理紋理的讀寫操作。當然在讀寫的過程中也會遇到一些性能瓶頸問題。這裏從讀和寫兩個部分分別來給出優化點:

  • Read

如上圖所示,我們可以嘗試使用mipmaps。此外,可以考慮更改過濾選項。例如,使用雙線性代替三線性,降低像素大小。確保使用了紋理壓縮,對Asset使用塊壓縮(如ASTC),對運行時生成的紋理使用無損紋理壓縮。

  • Write

如上圖所示,我們應該注意到像素的大小,以及每個像素中唯一MSAA樣本的數量。此外,可以嘗試一些優化一些邏輯寫法。

✎ Tile Memory Load and Store

圖塊內存是一組存儲Thread Group和ImageBlock數據的高性能內存。當從ImageBlock或是Threadgroup讀取或寫入像素數據時,比如在使用Tile着色器時或者是計算分派時,可以訪問到Tile內存。那當使用GPU性能計數器發現這個方面的性能瓶頸時,我們可以如下圖所示進行優化。

考慮減少threadgroup的並行,或者是SIMD/Quadgroup操作。此外,確保將線程組的內存分配和訪問對齊到16字節。最後,可以考慮重新排序內存訪問模式。

✎ Buffer Read and Write

在Metal中,緩衝區只被着色器核心訪問。在這個地方發現了性能瓶頸。我們可以如下圖所示進行優化:

可以更大力度的壓縮打包數據,例如使用例如packed_half3這樣小的類型。此外,可以嘗試向量化加載和存儲。例如使用SIMD類型。避免寄存器溢出,以及可以使用紋理來平衡工作負載。

✎ GPU Last Level Cache

如果在這個方面,我們的GPU性能計數器顯示一個過高的值。我們可以如下圖這樣優化:

如果紋理或者是緩存區也同樣顯示一個過高的值,我們可以把這個優化放到第一優先級。我們可以考慮減小工作集的大小。如果Shader正在使用Device Atomics,我們可以嘗試重構我們的代碼來使用Threadgroup Atomics。

✎ Fragment Input Interpolation

分段輸入插值。分段輸入在渲染階段由着色器核心進行插值。着色器核心有一個專用的分段輸入插值器。這個是比較固定和高精度的功能。我們能優化的點不多,如下圖所示:

儘可能的移除傳遞給分段着色器的頂點屬性。

內存帶寬

內存帶寬也是影響我們GPU性能的一個重要因素。如果在GPU性能計數器的內存帶寬模塊看到一個很高的值。我們就應該如下圖所示來進行優化:

如果紋理和緩存區也同樣顯示比較高的值,那優化優先級應該排到第一位。優化方案也是較少Working Set的大小。此外,我們應該只加載當前渲染過程需要的數據,只存儲未來渲染過程需要的數據。然後就是確保使用紋理壓縮。

Occupancy

如果我們看到整體利用率比較低,這意味着Shader可能已經耗盡了一些內部資源,比如tile或者threadgroup內存。也可能是線程完成執行的速度比GPU創建新線程的速度快。

避免重複繪製

我們通過GPU計數器可以統計到重複繪製的區域,我們應該高效使用HSR來避免這樣的重繪。我們可以如圖所示的順序來進行繪製。

Build GPU binaries with Metal

這一章主要給開發者們介紹了一種使用Metal的編程工作流,可以通過優化Metal的渲染編譯模型來增強渲染管線,這個優化可以在應用程序啓動,特別是首次啓動時大大減少PSO(管線狀態對象)的加載時間。可以讓我們的圖形渲染更加的高效。整個章節主要分爲四個部分:

Metal的Shader編譯模型概述

衆所周知,Metal Shading Language是Apple爲開發者提供的Shader編程語言,Metal會將編程語言編譯成爲一個叫做AIR的中間產物,然後AIR會在設備上進一步編譯,生成每個GPU所需的特定的機器碼。整個過程如下圖所示:

上述過程在每個管線的生命週期中都會發生,當前Apple爲了加速管線的重新編譯和重新創建流程,會緩存一些Metal的方法變體,但是這個過程還是會造成屏幕的加載耗時過長。而且在當前的這個編譯模型中,應用程序不能在不同的PSO(管線狀態對象)中重用之前生成的機器碼子程序。

所以我們需要一種方法來減少這個整個管線編譯(即源代碼->AIR->GPU二進制代碼)的時間成本,還需要一種機制來支持不同PSO之間共享子程序和方法,這樣就不需要將相同的代碼多次編譯或者是多次加載到內存中。這樣開發者們就可以使用這套工具來優化App首次的啓動體驗。

Metal二進制文件介紹

Metal二進制文件就是解決上述需求的方法之一,現在開發者們可以直接使用Metal爲二進制文件來控制PSO的緩存。開發者可以收集已編譯的PSO,然後將它們存儲到設備中,甚至可以分發到其他兼容的設備中(同樣的GPU和同樣的操作系統),這種二進制文件可以看做一種Asset。下面是一些例程和示意圖:

//創建一個空的二進制文件
let descriptor = MTLBinaryArchiveDescriptor()
descriptor.url = nil
let binaryArchive = try device.makeBinaryArchive(descriptor:descriptor)

//Populating an archive

// Render pipelines
try binaryArchive.addRenderPipelineFunctions(with: renderPipelineDescriptor)

// Compute pipelines
try binaryArchive.addComputePipelineFunctions(with: computePipelineDescriptor)

// Tile render pipelines
try binaryArchive.addTileRenderPipelineFunctions(with: tileRenderPipelineDescriptor)

//重用已編譯的方法

// Reusing compiled functions to build a pipeline state object from a file

let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
// ...
renderPipelineDescriptor.binaryArchives = [ binaryArchive ]

let renderPipeline = try device.makeRenderPipelineState(descriptor:  
                                                          renderPipelineDescriptor)
//序列化
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let archiveURL = documentsURL.appendingPathComponent("binaryArchive.metallib")

try binaryArchive.serialize(to: NSURL.fileURL(withPath: archiveURL))
//反序列化
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let serializeURL = documentsURL.appendingPathComponent("binaryArchive.metallib")

let descriptor = MTLBinaryArchiveDescriptor()
descriptor.url = NSURL.fileURL(withPath: serializeURL)
let binaryArchive = try device.makeBinaryArchive(descriptor: descriptor)

總的來說就是這個Metal二進制文件可以提供開發者手動管理管線緩存的方法,這樣就可以從一個設備中獲取這些文件並部署到其他兼容的設備上,在iOS環境下,極大地減少了第一次安裝遊戲或應用以及設備重啓後的管道創建時間。可以優化應用的首次啓動體驗和冷啓動體驗。

Metal對動態庫的支持

動態庫將允許開發者編寫可重用的庫代碼,卻可以減少重新編譯程序的時間和內存成本,這個特性將會允許開發者將計算着色器和程序庫動態鏈接。而且和二進制文件一樣,動態庫也是可序列化和可轉移的。這也是解決上述需求的方案之一。

在PSO生成的時候,每個應用程序都需要爲程序library生成機器碼,而且使用相同的程序庫編譯多個管線會導致生成重複的機器碼。由於大量的編譯和內存的增加,這個可能會導致更長的管線加載時間。而動態庫就可以解決這個問題。

Metal Dynamic Library允許開發者以機器碼的形式動態鏈接,加載和共享工具方法。代碼可以在多個計算管線中重用,消除了重複編譯和多個相同子程序的存儲。而且這個 MTLDynamicLibrary是可序列化的,可以作爲應用程序的Asset使用。MTLDynamicLibrary其實就是多個計算管線調用的導出方法的集合。

大致的工作流程如下:我們首先創建一個MTLLibrary作爲我們指定的動態庫,這個可以將我們的metal代碼編譯爲AIR。然後我們調用方法makeDynamicLibrary,這個方法需要指定一個唯一的installname,在管線創建時,linker將會使用這個名字來加載動態庫。這個方法可以將我們的動態庫編譯成爲機器碼。這就完成了動態庫的創建。

對於動態庫的使用來說:通過設置MTLCompileOptions裏的libraries參數,就可以完成動態庫的加載和使用了。代碼如下:

//使用動態庫進行編譯
let options = MTLCompileOptions()
options.libraries = [ utilityDylib ]
let library = try device.makeLibrary(source: kernelStr, options: options)

開發工具介紹

這個部分主要介紹了構建Metal二進制文件和構建動態庫的具體工具和方法。以視頻的形式可能會更好的表現,詳情可見:Build GPU binaries with Metal (從22:51開始)

Debug GPU-side errors in Metal

這一章主要介紹的是GPU側的bug,當前如果我們的應用程序出現了GPU側的bug,他的錯誤日誌常常都不能讓開發者很直觀的定位到錯誤的代碼範圍和調用棧。所以在最新的Xcode中,增強了關於GPU側的debug機制。可以像在代碼側發成的錯誤一樣不但能定位到錯誤原因,還有錯誤的調用堆棧和各種信息都可以詳細的查看到。讓開發者能更好的修復代碼造成的GPU側的渲染錯誤。

Enhanced Command Buffer Errors

這是當前的錯誤日誌上報,我們可以看到GPU側的錯誤日誌不像Api的錯誤日誌一樣可以讓開發者很快的定位到錯誤原因和錯誤的代碼位置。

而最新的Metal debugging工具就增強了這方面的能力,讓Shader的code也可以像Api代碼一樣提供錯誤定位和分類能力。

我們通過以下代碼便可以啓用增強版的commandbuffer錯誤機制

//啓用增強版的commandbuffer錯誤機制

let desc = MTLCommandBufferDescriptor()
desc.errorOptions = .encoderExecutionStatus
let commandBuffer = commandQueue.makeCommandBuffer(descriptor: desc)

錯誤一共有五種狀態:

我們也可以通過以下代碼來打印error:

//打印commandbuffer的錯誤
if let error = commandBuffer.error as NSError? {

    if let encoderInfos =
        error.userInfo[MTLCommandBufferEncoderInfoErrorKey]
        as? [MTLCommandBufferEncoderInfo] {

        for info in encoderInfos {
            print(info.label + info.debugSignposts.joined())
            if info.errorState == .faulted {
                print(info.label + " faulted!")
            }
        }
    }
}

開發者可以在開發時和測試時啓用優化版的錯誤機制

Shader Validation

如上圖所示,這個功能可以在GPU側發生渲染錯誤時自動定位和catch到錯誤並定位到代碼,以及獲取回溯棧幀。

我們可以在Xcode中按照以下流程來開啓這個功能:

1.開啓Metal中的兩個Validation選項

2.開啓issue自動斷點開關並配置類型和分類等選項

Video中用了一個demo來展示整個工作流,具體參見Debug GPU-side errors in Metal(11:25~14:45)大致流程如下圖所示:

這是一個Demo應用程序,很明顯它在渲染上出現了一些異常,但是因爲是GPU側的問題,所以開發者很難定位。但是通過上述的工作流開啓Shader Validation之後。

Xcode會自動斷點到發生異常的地方,並展示出異常信息,這樣就可以極大的提升開發者的錯誤修復效率。

Gain insights into your Metal app with Xcode 12

這一章主要講的是Xcode12給Metal App提供了更多調試和分析的新工具。大致如下圖所示:

主要分爲兩個部分:

Metal Debugger

這個工具可以讓開發者在App運行時,獲取到想分析和調試的任何一幀,然後再進入Xcode提供的各種分析界面,總體情況,依賴情況,內存,帶寬,GPU,Shader等各種具體的界面來對這一幀進行更加詳細的分析和調試。整個過程使用視頻的方式可能會更加高效,所以這裏不會進行詳細的贅述和分析。詳情可以參見 Gain insights into your Metal app with Xcode 12

Metal System Trace

整個工具跟之前提到過的Debugger相比,他的功能主要是讓開發者可以隨着時間的推移來捕獲應用程序的各種信息和特徵,可以讓開發者很好的調試一些例如終端,幀丟失,內存泄漏等問題。而Debugger主要是對某一幀進行調試和分析。

他提供了一個叫做編碼時間線的工具,可以讓開發者查看到GPU在應用運行中的運行各種命令緩衝的情況。然後提供了一個叫做着色器時間線的工具,可以讓開發者查看到各種着色器在代碼運行期間運行的過程。然後還有GPU計數器的工具,這個工具我們在前文進行了詳細的分析,主要是用於解決GPU的繪製性能問題的工具。然後最後一個工具就是內存分配跟蹤工具,可以讓開發者查看到應用程序運行過程中各種內存的分配和釋放,可以幫助開發者解決內存泄漏問題或者是降低應用內存佔用。

技術啓發與思考

WWDC 20關於Metal的Session中,比較重要的就是官方提供了很多可供開發者進行GPU級別的調試工具以及性能分析工具。給比較成熟龐大而複雜的工程突破性能瓶頸,提供更加優秀的用戶體驗提供了一些思路。

閒魚作爲一個電商類App,隨着功能和增多和以及工程的複雜化,在所難免的會遇到性能瓶頸,而閒魚團隊當前面對挑戰的方式是從工程級別來進行優化。從Flutter的角度來看,WWDC 20 對於Metal的調試工具和性能分析工具的完善,無疑提供了更多的優化思路。這爲未來運行在iOS上的應用的調優和突破性能瓶頸帶來了新的思路和可能性。

對於跨平臺框架,Apple有自家的SwiftUI,這也是此次大會的重點項目。不過無論是Flutter,還是SwiftUI,大家最後對應用的性能瓶頸突破和優化一定是殊途同歸的,也就是深入到GPU級別來進行開發和調試以及性能分析。對於未來的客戶端開發人員,理解GPU和進行GPU級別的編程肯定是不可或缺的技能點之一。

鏈接

本文轉載自公衆號淘系技術(ID:AlibabaMTT)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650408962&idx=1&sn=03f42f1d466d226ee21757b9021db3a6&chksm=8396c01ab4e1490c2956cd28ed8051b33f8d8fe56c6c432e12422aacbce58a20d436d3f5bde1&scene=27#wechat_redirect

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章