游戲幀率突然暴跌,你第一反應是不是“Unity又抽風了”?或者覺得手上這塊顯卡該換了?但一臺臺高性能設備送進測試機,一條條profiler數據拉出來,真相往往讓人哭笑不得——元兇就藏在你自己寫的C#代碼里,幾行看似不痛不癢的臨時數組操作,正在堆上瘋狂分配內存,把垃圾回收器(GC)變成幀率殺手。
這事兒用一張圖就能說清楚。想象你的內存是整整齊齊的倉庫,需要處理其中一小段貨架上的貨物。傳統的做法是:把整段貨架的貨物原樣復制一份到新推車上,干完活再扔掉,這一“復制-丟棄”就是一次堆分配。在幀循環或高頻率Job里重復這個動作,分配的“小推車”很快就堆滿,觸發GC上門打掃。GC一來,CPU就得停下手里的渲染活兒去清理,反映到屏幕上就是那一下惱人的頓挫。
Span和Memory這套組合披的就是“停止復制”的斗篷。它們不搬運數據,只給你開一個零分配的“觀察窗”——直接指向現有的連續內存,讓你在窗口內想怎么處理就怎么處理,完全不用拷貝。在Unity的高性能場景里,這個思維轉彎能直接抹掉一大類GC壓力。
先看Span。它是一個ref struct,只能活在棧上,不能被裝箱,也不能鉆進異步方法。這個“棧釘子戶”的身份看起來很受限,卻正是它極致性能的來源:用完即焚,絕不給GC留一點負擔。當你需要從一個大數組或NativeArray里切出一段來干活時,一個Slice調用拿到的就是一個Span視圖,背后沒有new,沒有分配。像下面這段Job代碼,inputSegment和outputSegment不過是往原有數據上蓋了兩扇窗,數據還在原地,GC毫無壓力。
public struct MyDataProcessorJob : IJob[ReadOnly] public NativeArray InputData;public NativeArray OutputData;public void Execute()Span inputSegment = InputData.Slice(10, 20);Span outputSegment = OutputData.Slice(0, 20);// 在視窗內處理 inputSegment 和 outputSegment那如果場景不讓你死守棧上,比如需要把視圖存到字段里,或者要在異步方法里傳遞,這時候Memory就登場了。它是一個可以逃逸到堆上的包裝,內部裹著一個Span,讓你在Span不能去的場合依然享受零分配視角。你可以先通過Memory拿到一個安全引用,要干活時再調它的.Span屬性回到棧上快速處理,靈巧地繞過ref struct的限制。
說到底,很多人抱怨Unity的GC不可控,是因為沒搞明白這些分配究竟從哪里冒出來。Array.Copy、Linq里隨手一個Skip().Take().ToArray(),這些習慣性動作都在歡快地生成臨時對象。現在Span和Memory已經把“不復制只觀察”的能力擺到了C#里,再用起來,那些莫名其妙的GC Spike就真的可以對著鏡子找原因了。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.