今天和大家分享一個優(yōu)化經(jīng)驗只磷,主要關于獲取一個資源的依賴資源列表即對AssetDatabase.GetDependencies這個接口的調(diào)用效率優(yōu)化泌绣。通過一步步優(yōu)化最后在對工程中所有資源獲取依賴資源的執(zhí)行上提升了近100倍的效率。
在對AssetBundle進行打包時候元媚,需要獲取資源的依賴關系苗沧,并生成最后所有資源的BundleName。這里主要的瓶頸就是對資源的依賴關系數(shù)據(jù)獲取上鞠绰。在工程實踐中發(fā)現(xiàn)整個構建環(huán)節(jié)20分鐘飒焦,16分鐘是BuildAssetBundles開銷,3分鐘是GetDependencies開銷翁巍。在增量構建中休雌,BuildAssetBundles可降為1-3分鐘,而GetDependencies則仍需要3分鐘開銷杈曲。當然對于資源數(shù)量較小的工程,這個優(yō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棒拂,通過緩存每次結果下次查詢時可以立即返回結果。不過由于資源會修改帚屉,依賴文件會發(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ù)的加載和保存時間卻被忽略了最冰。實際結果是這部分的數(shù)據(jù)量較大,加載和保存開銷也比較大赌朋,如果使用Json來存儲的話篇裁,這里大概要花費1.5mins來讀寫這數(shù)據(jù)。
這里討論下對這個數(shù)據(jù)存儲效率的優(yōu)化达布,首先來看看數(shù)據(jù)結構:
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)了兩次,如果能把消除重復字符串瑟枫,則可以近一步較少文件大小,提高讀寫速度慷妙。
修改后的結構如下:
public class DepenData
{
public int assetPathIndex;
public Hash128 assetDependencyHash;
public int[] dependsPathIndex;
public string[] dependsPath; // 用于返回查詢結果,不保存
}
// 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ù)結構組織贞绵,填充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ù)結構還可以再改進......
把二進制文件壓縮后從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)化是基于專注性思維的思考結果析珊,而三則是發(fā)散性思維的思考結果。專注性思維容易陷入思維定式蔑穴,這時候可以起來喝杯茶,出去散散步惧浴。
[完 Carber 2018-08-12]