Unity中資源依賴關系獲取優(yōu)化

今天和大家分享一個優(yōu)化經(jīng)驗,主要關于獲取一個資源的依賴資源列表即對AssetDatabase.GetDependencies這個接口的調(diào)用效率優(yōu)化。通過一步步優(yōu)化最后在對工程中所有資源獲取依賴資源的執(zhí)行上提升了近100倍的效率。

在對AssetBundle進行打包時候,需要獲取資源的依賴關系,并生成最后所有資源的BundleName。這里主要的瓶頸就是對資源的依賴關系數(shù)據(jù)獲取上。在工程實踐中發(fā)現(xiàn)整個構(gòu)建環(huán)節(jié)20分鐘,16分鐘是BuildAssetBundles開銷,3分鐘是GetDependencies開銷。在增量構(gòu)建中,BuildAssetBundles可降為1-3分鐘,而GetDependencies則仍需要3分鐘開銷。當然對于資源數(shù)量較小的工程,這個優(yōu)化就是一個可有可無的選項對構(gòu)建速度影響不大。

還有一個常見的應用場景就是快速查找資源資源的依賴數(shù)據(jù)以及被依賴數(shù)據(jù),也可以通過這次的優(yōu)化帶來體驗上提升。

首先從分析AssetDatabase.GetDependencies這個接口的行為開始,簡單的編寫一個測試函數(shù):

public static void Test()
{
    long timeStamp = Stopwatch.GetTimestamp();
    string[] dir = Directory.GetFiles("Assets/", "*.*", SearchOption.AllDirectories);
    for (int i = 0; i < dir.Length; ++i)
    {
        if (dir[i].EndsWith(".meta", System.StringComparison.OrdinalIgnoreCase))
        {
            continue;
        }
        AssetDatabase.GetDependencies(dir[i], true);
    }
    UnityEngine.Debug.LogFormat(
        "GetDependencies cost {0} ms.", 
        (Stopwatch.GetTimestamp() - timeStamp) * 1000 / Stopwatch.Frequency
    );
}

通過執(zhí)行這個函數(shù)可以了解這個函數(shù)的開銷,以及獲得數(shù)據(jù)為之后的優(yōu)化做對比。

第一次執(zhí)行的時候較慢,有較高的硬盤讀寫,總共花費6.3mins。

第二次執(zhí)行的時候快了近一倍,基本無硬盤讀寫,總共花費3.2mins。

這里硬盤是使用SSD,如果使用機械鍵盤則這里性能堪憂,第二次基本所有內(nèi)容都進了內(nèi)存,操作系統(tǒng)做了緩存,所以快了很多。所以換更好的硬盤可以提高這里的效率,不過現(xiàn)在的執(zhí)行時間還是太長了。

如果對所有的資源進行掃描通過GUID去查詢并獲取依賴關系,那應該不止這么點時間。這里猜測Unity做過一些數(shù)據(jù)預處理與緩存來優(yōu)化這個接口效率。

這時候第一個優(yōu)化思路是Cache,通過緩存每次結(jié)果下次查詢時可以立即返回結(jié)果。不過由于資源會修改,依賴文件會發(fā)生變化,所以緩存可能會出錯。如果不能判斷當前緩存是否有效,則只能在確保資源部修改的情況下使用緩存數(shù)據(jù)。

這里使用AssetDatabase.GetAssetDependencyHash來驗證緩存是否有效,這個接口返回Asset的一個Hash值(包括文件名以及meta文件),如果Hash值不變,我們可以認為這個Asset直接依賴的資源文件不變,由于直接依賴是通過Asset文件內(nèi)部的GUID索引的,所以Hash不變即表示GUID不變,即依賴關系不變。這里緩存Hash值以及這個Asset的直接依賴。通過所有的直接依賴,可以快速的計算出這個Asset的全部依賴。

AssetDatabase.GetAssetDependencyHash接口非常高效,這里簡單討論下。

這部分數(shù)據(jù)在Import Asset的時候計算并緩存,所以可以高效獲取。每個Asset都有自己的AssetDependencyHash,Reimport的時候重新計算。這里判斷文件是否修改的依據(jù)是文件最后修改時間是否發(fā)生變化。獲取目錄下所有文件信息由于有操作系統(tǒng)文件系統(tǒng)做了索引是非常高效的。

由于Refresh是一個必要項,這項開銷已經(jīng)花費出去了,所以這里可以直接享受接口的高效率。

最后只要把每次的數(shù)據(jù)保存在本地,下次使用的時候先從本地加載,即可使這部分邏輯時間優(yōu)化到2800ms左右,優(yōu)化了近100倍,這里的執(zhí)行效率已經(jīng)非常優(yōu)異了,主要開銷在GetFiles上。

上面只討論了有緩存數(shù)據(jù)情況下的優(yōu)化情況,但實際緩存數(shù)據(jù)的加載和保存時間卻被忽略了。實際結(jié)果是這部分的數(shù)據(jù)量較大,加載和保存開銷也比較大,如果使用Json來存儲的話,這里大概要花費1.5mins來讀寫這數(shù)據(jù)。

這里討論下對這個數(shù)據(jù)存儲效率的優(yōu)化,首先來看看數(shù)據(jù)結(jié)構(gòu):

public class DepenData
{
    public string assetPath;
    public Hash128 assetDependencyHash;
    public string[] dependsPath;
}
// save data
Dictionary<string, DependData> m_data;

基本都是字符串數(shù)據(jù),存儲出來的文件都有300M左右(大概,具體忘了),把Json存儲改為二進制以后,文件大小縮減為75M左右,加載時間從1.5mins變成了18.3S。較大的改進,不過還可以在改進我想。

這里依賴數(shù)據(jù)是遞歸即 A依賴B,B依賴C,在A的DependData里面就會有ABC,而B的依賴數(shù)據(jù)里面有BC。這里可以發(fā)現(xiàn)BC出現(xiàn)了兩次,如果能把消除重復字符串,則可以近一步較少文件大小,提高讀寫速度。

修改后的結(jié)構(gòu)如下:

public class DepenData
{
    public int assetPathIndex;
    public Hash128 assetDependencyHash;
    public int[] dependsPathIndex;
    public string[] dependsPath; // 用于返回查詢結(jié)果,不保存
}
// save data
Dictionary<string, DependData> m_data;
List<string> m_strList;
// temp data
Dictionary<string, int> m_strIndex;

改進后文件大小變?yōu)?8M,加載時間從18.3S優(yōu)化到3.3S。6倍的改進,挺棒的,這時候又在思考是否有改進的余地。

第一個改進,把FileStream改為MemoryStream,數(shù)據(jù)則通過File.ReadAllBytes()讀取。這個改造可以把3.3S改進為3S,主要是由于FileStream API調(diào)用的效率并不高,這里是通過減少調(diào)用頻率來改進效率。對于FileStream每次ReadByte(2)和每次ReadByte(1024),可能有接近100倍的性能差異。

第二個改進,分析發(fā)現(xiàn)3S里面BinaryReader占用了2.7S,剩下數(shù)據(jù)結(jié)構(gòu)組織,填充Dictionary占用了0.3S。C#的BinaryReader實現(xiàn)并不高效,可以通過更高效的序列化數(shù)據(jù)方式來優(yōu)化。這里嘗試使用了FlatBuffers來替換BinaryReader,保存的開銷從880ms增長到1200ms,讀取的時間從3000ms優(yōu)化到1200ms。又是一次大幅度的優(yōu)化,雖然現(xiàn)在收益時間已經(jīng)無關緊要了,不過實踐和驗證想法也是不錯的收獲。這里開啟FlatBuffers Unsafe模式應該會有更高的收益,接近C++的性能,如果直接用C++寫性能果然會好很多吧。

Unity所有路徑都是Assets開頭,大量路徑字符串里面前綴包含重復數(shù)據(jù),數(shù)據(jù)結(jié)構(gòu)還可以再改進......

把二進制文件壓縮后從28M變成5M,確實很多冗余數(shù)據(jù),不過再改進可能付出太多時間而受益太低。這次的優(yōu)化就到此為止了嘛。

這里上最后一個優(yōu)化思路異步化

異步加載在游戲中是很常見的做法,所以這里其實再實現(xiàn)兩個異步化接口即可把這部分時間優(yōu)化為0,由于還有其他很多任務可以并行執(zhí)行,所以這部分時間在調(diào)整到適當?shù)臅r機后可以忽略不計。

由于Unity的接口不能在多線程調(diào)用,所以一開始就不會往這個方面思考,后面問題轉(zhuǎn)化后異步是一個非常優(yōu)異的做法,F(xiàn)latBuffers的改造非常繁瑣,浪費了我大量測試時間。最后我把代碼回滾到二進制版本,F(xiàn)latBuffers在運行時確實能帶來巨大的效率提升,不過這里可能并不需要上這個利器了。

一、二的優(yōu)化是基于專注性思維的思考結(jié)果,而三則是發(fā)散性思維的思考結(jié)果。專注性思維容易陷入思維定式,這時候可以起來喝杯茶,出去散散步。

[完 Carber 2018-08-12]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容