2017年4月24日 星期一

Asset Bundles與Resources的記憶體大對決

作者:Ryan Caltabiano 原文
潤稿:Kelvin Lo

*連結全部導向英文頁面

近期有不少Unity開發者詢問有關Asset Bundle和舊的資源系統Resources的相關問題:為何Asset Bundle載入Asset時消耗的記憶體要比Resources多。


首先說明事實並非如此。或者說從長遠來看,如果用好Asset Bundle中一些Resources系統所沒有的新特性,Asset Bundle的記憶體消耗會小的多。如果您不熟悉Asset Bundle,可以參考Unity手冊Asset BundleResources指南

根據我們收到的一些Bug回報,基本上說的都是同一件事情:當從一個Asset Bundle載入某個Asset時,記憶體使用量會衝高好幾MB,但在使用Resources時卻沒發生這樣的情況。我們嘗試重現這些問題時,看到的結果也非常相似:啟動時記憶體正常,載入Asset後記憶體激增,並且不會回到原來的水平。

Asset Bundle的記憶體使用情況



Resources的記憶體使用情況




下面我們就透過記憶體系統就關聯關係、資料保存方式、記憶體使用量的含意和記憶體使用效率幾個方面來分析一下這個問題的原理。注意:本文使用的Unity版本為Unity 5.5.0f3。

Unity的原生記憶體系統會使用1MB到32MB(平均1MB到4MB)之間幾個不同固定大小的塊記憶體分配器。具體大小根據分配的工作類型而定,例如主執行緒還是背景執行緒或根據當下執行的平台而定。

保留總量(Reserved Total)是作業系統分配的所有區塊的總量,已用總量(Used Total)是其中正由Unity使用的記憶體量。每個區域標籤、FMOD、Porfiler等等,表示系統列出的相應分配器或大概的外部記憶體。這些區域標籤資訊在Memory Profiler手冊頁面中可以查詢。有幾點手冊上沒說明的,像是Used and Reserved totals並不包含FMOD數值(Unity 5.5.0f3),
這問題我們已經有個修復更新。

總系統記憶體使用量(Total System Memory Usage)是由平台系統提供的虛擬記憶體大小,不支援此功能的平台會顯示為0。最後,已用總量並不包括物件的標頭(Header)或位元組對齊(byte alignment),但會保留總計。因此,要對比Asset Bundle和Resources間的記憶體使用量,我們會主要針對已用總量和保留總量中的Unity區域標籤。

另外,瞭解Asset Bundle以及Resources資料在磁碟上的保存方式對於理解分析器的原理也十分必要。

Resources和Asset Bundle在底層資料結構上非常相似,它們都有一個用來存放每個物件序列化資料的檔,一些用來有效率非同步載入的額外資源檔(貼圖、音效等),以及一個包含序列化物件Asset檔路徑
映射表。Asset Bundle將這些檔都打包在一個壓縮包中,映射表存在Asset Bundle物件的序列化資料中。Resources將映射表保存在一個名為ResourceManager的全域單例(global singleton)中,其他檔則散落在磁碟各處。此外,與Resources系統不同的是,Asset可以分散在不同的Asset Bundle中,因此可以透過載入資料子集來最大限度地減少記憶體使用量。關於Asset Bundle的內部結構資料可以參閱Unity手冊

Asset Bundles



Resources




瞭解Unity的記憶體與檔案系統之後,我們再詳細解釋下這些記憶體使用量數值的意義。

第一個凸顯的問題是Unity用於Asset Bundle的保留區域多了10MB,而用於Resources系統的則沒有增長。這是為什麼呢?這主要是因為前面提到的區塊記憶體分配器。就這個特定的記憶體使用率測試而言,我們使用的是非同步載入API 協程(Coroutines)的AsyncBundleLoader.cs行為。值得注意的是,這種組合實際上會使用不同的區塊記憶體分配器,這些區塊記憶體分配器在此時其實尚未被使用。所以,10MB的占用源於兩個區塊記憶體分配器初始化區塊的行為,其中一個區塊記憶體分配器需要為新物件分配更多記憶體,所以它分配了一個4MB的區塊。兩個新分配器中,一個為Asset Bundle非同步載入分配了一個2MB的區塊,另一個則為類型樹(type trees)分配了一個4MB的區塊。這些區塊的大小是專為同時載入多個Asset與Asset Bundle而優化的。例如,您可以同時從4 - 5個Asset Bundle中載入物件,而無須為Asset Bundle非同步載入或需要新塊的類型樹創建新的分配器。當然,這具體還是要根據Asset Bundle的大小,採取的壓縮方式,以及這些包中所使用的唯一腳本類型的數量而定。

在這些分配的區塊中,用於類型樹的4MB區塊,僅在從Asset Bundle載入物件時使用。操作完成後這個塊理論來說會釋放。但是,由於範例中構建協程的方式,導致AssetBundleRequest物件一直處於作用中,沒有被垃圾回收器(GC)清除。而用於Asset Bundle非同步載入的2MB區塊,它是讀取Asset Bundle檔案時的緩衝區,在沒有包的內部引用後會被釋放。最後的4MB區塊的使用者是負責我們所有物件存儲的主分配器,因此不會被釋放。

通常在一個專案中,物件的建立/刪除非常頻繁,我們會使用記憶體池來重用記憶體而非將它釋放回分配器。觀察最終卸載後保留區域的Unity數值時,您會發現使用Asset Bundle(64.1MB)與使用Resources系統(63.3MB)的差異很小,和區塊記憶體分配器獲得新塊的順序有關。

我們一直都在討論保留記憶體,那Asset Bundle與Resources之間的保留記憶體實際使用效率差別又有多大呢?

這個問題非常簡單,因為已使用中的Unity區域已經告訴了我們答案。使用Asset Bundle時,佔用了保留記憶體中的21.7MB,而使用Resources時稍多,大概在22.2MB。此外,在卸載時,這個記憶體數值分別下降為20.7MB和21.2MB。所以,很顯然Asset Bundles是記憶體利用效率方面的贏家。

你可能已經注意到,Asset Bundle的使用量在卸載後要比它啟動時更大(4.4MB)。這是因為前面提到的記憶體池的關係,因此如果您重新載入Asset Bundle與Asset,它將會回到21.7MB。而對於Resources來說,啟動與卸載時的記憶體差異來自四捨五入誤差。對於Asset Bundle的塊記憶體分配可以減少,但是要犧牲效能與向下相容性。正如上面提到的,為了滿足載入物件所需的記憶體量,所以必須要分配4MB大小的區塊。剩下的6MB中,2MB用於了非同步載入API。

因此,為了防止區塊記憶體分配,只要犧牲FPS使用同步API即可。最後的4MB是源於類型樹的分配。這個系統會在Asset Bundle裡儲存Resources系統中沒有的額外資料,這些資料使Asset Bundle可以相容更多版本的Unity,並使諸如FormerlySerializedAs這樣的序列化特性正常工作。這使您可以在升級到更新的Unity版本後依然能使用相同的Asset Bundle,或只需修改少量程式,而不是重新打包,導致開發者因為更新必須重新下載整個Asset Bundle。向BuildPipeline.BuildAssetBundlesAPI傳入BuildAssetBundleOptions.DisableWriteTypeTree選項,可以禁止寫入這個額外資料。

無類型樹Asset Bundle同步載入



您可以用Asset Bundles 1, Resources 0試試。如果您想產生自己的資料,這個範例專案已經上傳到了 Github 上。目前它設定為亂數取樣每種建立100個:Textures、Monobehavior、Prefabs,所以在自己機器上每次執行都會產生同樣的輸出(但會和別人不一樣)。請確保你的Asset Bundle專案沒有意外包含了一個有內容的Resources資料夾,否則您的記憶體將會比預期高出兩倍。試試將每類資源的數量提高至300或甚至500個測試。

2017年4月17日 星期一

Draw Call未被批次處理?告訴你在Unity 5.6中如何找出原因

作者:Valentin Simonov 原文
潤稿:Kelvin Lo


相信各位都知道Unity內建動態和靜態批次處理,能有效地降低Draw Call的數量。

當你查看Stats介面時,如果看到一個標為"Saved by batching"的值,它就是用來顯示批次處理的次數。可惜它很難用來反推為何批次沒有被處理。儘管Unity手冊裡有說明可能的發生原因,但要理解這些資訊需要開發者已有相關的基礎知識。

好裡加在,Unity5.6 在Frame Debugger中新增了一項功能,能解釋這些批次資訊。

Frame Debugger是Unity 5.x推出的功能,你可以點功能表的Window > Frame Debugger 來打開Frame Debugger。它能顯示遊戲中所有的批次處理資訊,以及這些批次處理的所有細節資訊,包括著色器、貼圖及批次處理所用的大量資訊等。

在詳細介紹Unity何時發起新的批次處理之前,我們先來瞭解批次處理的概念及作用。

Unity 5.6中的Frame Debugger,這裡說明為何Unity要發動批次處理



批次處理 - Batch


Unity為了在螢幕上繪製物件,它需要向圖形API發起一次“繪製”命令,就是一次“Draw Call”。但在發起命令之前,Unity還需要為繪製的物件設定所需的GPU狀態:網格、著色器、貼圖、混合設定以及一些其他的著色器屬性。而狀態改變命令再加上一個或多個繪製命令就稱為一次批次處理(a Batch)



批次處理過程 - Batching


導致批次處理緩慢的原因就是改變GPU狀態的指令,而繪製指令實際上僅佔用很少的資源。所以Unity總是試圖利用同一個GPU狀態同時渲染多個物件。這一過程被稱為批次處理(Batching)。


Unity提供三種類型的批次處理:靜態批次處理(Static batching),動態批次處理(dynamic batching)以及GPU Instancing。

  • 靜態批次處理會在構建時將多個靜態網格物件合併為一個或多個大的網格物件,然後在運行時一次批次處理渲染一個大網格中的多個物件。
  • 動態批次處理在每幀中獲取多個小型網格物件,在CPU中對其進行頂點變換,將相似的頂點組合到一起,然後一次繪製它們。
  • GPU Instancing(Unity 5.3導入)可以利用少量Draw Call繪製多個具有不同的位置、旋轉以及其他著色器屬性的相同物件。


導致批次處理失敗的原因


有時在編輯器中可以清楚地看到,一些本應被批次處理的物件出於某些原因沒有被批次處理。首先,請檢查Player Settings中是否啟用批次處理功能。這個步驟看似多餘,但我們遇到太多的無法處理的原因都是因為忘記開啟。

我們專門為此提供了展示專案來演示Unity在什麼情況下必須發起新的批次處理請求。首先下載專案並複製到Unity專案中。請注意,你需要安裝Unity 5.6才能看到Frame Debugger中關於批次處理狀態的說明。


以下是展示專案(Unity 5.6)中導致無法進行批次處理的原因。每個原因對應一次單獨的批次處理:

  • Additional Vertex Streams — 物件使用MeshRenderer.additionalVertexStreams設定了額外的頂點資訊流。
  • Deferred Objects on Different Lighting Layers — 該物件位於另一不同的光照層中。
  • Deferred Objects Split by Shadow Distance — 兩個物體中有一個在陰影距離範圍內而另一個不是。
  • Different Combined Meshes — 該物件屬於另一個已合併的靜態網格。
  • Different Custom Properties — 該物件設定了不同的MaterialProperyBlock。
  • Different Lights — 該物件受不同的前向光照(Forward Light)影響。
  • Different Materials — 該物件使用不同的材質。
  • Different Reflection Probes — 該物件受不同的反射探頭(Reflection Probe)影響。
  • Different Shadow Caster Hash — 該物件使用其他的陰影投射著色器,或是設定了不同的著色器參數/關鍵字,而這些參數/關鍵字會影響陰影投射Pass的輸出。
  • Different Shadow Receiving Settings — 該物件設定了不同的“Receive Shadows”參數,或是一些物件在陰影距離內,而另一些在距離之外。
  • Different Static Batching Flags — 該物件使用不同的靜態批次處理設定。
  • Dynamic Batching Disabled to Avoid Z-Fighting — Player Settings中關閉了動態批次處理,或在當前環境中為避免深度衝突而被臨時關閉。
  • Instancing Different Geometries — 使用GPU Instancing渲染不同的網格或子網格。
  • Lightmapped Objects — 物件使用了不同的光照貼圖,或在相同的光照貼圖中有不同的光照貼圖UV轉換關係。
  • Lightprobe Affected Objects — 物件受其他光照探頭(Light Probe)影響。
  • Mixed Sided Mode Shadow Casters — 物件的“Cast Shadows”設定不同。
  • Multipass — 物件使用了帶多個Pass的著色器。
  • Multiple Forward Lights — 該物件受多個前向光渲染影響。
  • Non-instanceable Property Set — 為instanced著色器設定來non-instanced屬性。
  • Odd Negative Scaling — 該物件的縮放為很奇怪的負值,例如(1,-1,1)。
  • Shader Disables Batching — 著色器使用“DisableBatching”標籤顯式關閉了批次處理。
  • Too Many Indices in Dynamic Batch — 動態批次處理索引過多(超過32k)。
  • Too Many Indices in Static Batch — 靜態批次處理中的組合網格索引過多。對於OpenGL ES來說是48k,OSX是32k,其他平台是64k。
  • Too Many Vertex Attributes for Dynamic Batching — 欲進行動態批次處理的子網格擁有超過900個頂點屬性。
  • Too Many Vertices for Dynamic Batching — 欲進行動態批次處理的子網格頂點數量超過300個。

結論


現在可以開始使用Frame Debugger新功能來檢查你的專案,看看是否能找到可以優化的線索。隨著引擎不斷更新,將來也會加入更多批次處理的資訊。

這裡是展示專案的Github,如果你有興趣也可以關注一下。

著作人