返回所有文章

Unity 的「記憶體使用量」該看哪個數字?八項測量與 Framedash 回報值

同一個 Unity 遊戲,在 Framedash 儀表板、工作管理員和 C# 堆積計數器中顯示的記憶體使用量可能相差很大。我們在 Windows 上執行繪製 20,000 個立方體的示範,讀到了 167 MB、739 MB 和 8 MB。這三個數字來自不同的觀測:儀表板和堆積的數字是兩次不同執行的中位數,工作管理員的 739 MB 則是某一時刻的讀值。

比較之前,先要確認各個計數器統計什麼。Framedash 的 Unity SDK 回報的是 Unity 配置器正在使用的記憶體,與受控堆積使用量、行程占用的實體記憶體都不同。為了在相同條件下比較這些計數器,我們對同一個建置逐影格記錄了八項讀值。

記憶體使用量的四種統計範圍

這些讀值可以分為四類。用「層」來理解有助於區分用途,但它們並非嚴格的包含關係。

  • 受控堆積:存放 C# 類別執行個體、陣列等物件,由記憶體回收行程管理的記憶體
  • Unity 已配置記憶體:Unity 配置器記憶體池中正在使用的部分,包含紋理資料等受控堆積以外的引擎記憶體配置
  • Unity 已保留記憶體:Unity 向 OS 取得並保留的整個記憶體池,包括供後續重複使用的可用容量
  • 行程記憶體:OS 觀點下的記憶體,統計範圍還包括執行檔與 DLL 的對應等 Unity 配置器未追蹤的部分

OS 也不只有一種統計方式。工作集統計目前常駐在實體記憶體中的分頁;私人認可量統計行程專用的已認可記憶體,包括已換出的部分。本次測試的 OS 讀值大於 Unity 的讀值,但不能把不同範圍的數字相減,就當作某一類記憶體的準確用量。圖形驅動程式記憶體也是獨立的計數器,並不是包在外面的第五層。

用兩種方式繪製 20,000 個立方體

測試使用上一篇文章的專案,以兩種方式繪製同樣的 20,000 個立方體:

  • Naive:每個立方體有一個 GameObject 和一個 MeshRenderer
  • InstancedDirect:不為每個立方體建立 GameObject,由指令碼呼叫 Graphics.RenderMeshInstanced 繪製

兩者對應上一篇的 NAIVE 和 INSTANCED_DIRECT。環境為 Unity 6000.4.11f1、Built-in Render Pipeline、D3D12、Mono 指令碼後端、RTX 4070 Ti、1920x1080。主要比較表採用關閉遙測傳送的 Release Player 資料。

每個模式暖機 15 秒,再測量 120 秒。在這次實驗中,八項讀值都在 SDK 的 Collect() 呼叫結束後、同一影格內擷取。表中每個值是對應計數器在測量期間所有影格中的中位數(p50),並不共同代表某個影格的狀態。

本次測試的 Mono Player 中,System.Diagnostics.Process.WorkingSet64 和 PrivateMemorySize64 都回傳 0。我們改用 P/Invoke 呼叫 psapi 的 GetProcessMemoryInfo,並與 Player 外部執行的 Get-Process 交叉核對。核對得到的中位數分別為 848.5 MB 和 848.7 MB,相差 0.2 MB。

八項讀值,各自取 120 秒的中位數

讀取方式 Naive InstancedDirect 變化
GetMonoUsedSizeLong 8.2 MB 6.9 MB -16%
GC.GetTotalMemory(false) 8.2 MB 6.9 MB -16%
GetMonoHeapSizeLong 10.3 MB 7.9 MB -23%
GetTotalAllocatedMemoryLong 167.2 MB 56.6 MB -66%
GetTotalReservedMemoryLong 263.4 MB 155.2 MB -41%
OS 工作集 852.4 MB 412.0 MB -52%
OS 私人(認可) 1,168.3 MB 681.9 MB -42%
GetAllocatedMemoryForGraphicsDriver 0 0 -

帶底色的列是 Framedash Unity SDK 回報的計數器。Naive 和 InstancedDirect 的影格時間 p50 分別為 9.25 ms 和 1.44 ms,與上一篇的趨勢一致。上一篇的 169.0 MB 和本次的 167.2 MB 來自相同建置組態、相同啟動引數下的兩次獨立執行,統計的都是已配置記憶體。

把六項讀取按 Naive 與 InstancedDirect 並排畫出的橫向長條圖。由上而下依序是 GetMonoUsedSizeLong 8.2 / 6.9 MB、GetMonoHeapSizeLong 10.3 / 7.9 MB、帶底色的 GetTotalAllocatedMemoryLong 167.2 / 56.6 MB、GetTotalReservedMemoryLong 263.4 / 155.2 MB、OS 工作集 852.4 / 412.0 MB、OS 私人(認可)1,168.3 / 681.9 MB
刻度是線性的。八項讀取裡畫出了六項:GC.GetTotalMemory(false) 在這個位數上和 GetMonoUsedSizeLong 相同,GetAllocatedMemoryForGraphicsDriver 在 Release 播放器裡是 0。深色長條是 Naive,淺色長條是 InstancedDirect,帶底色的那一列就是 Framedash 的 Unity SDK 送出的那一層。

從 Naive 切換到 InstancedDirect 後,Unity 已配置記憶體減少 66%,受控堆積使用量減少 16%,受控堆積容量減少 23%。三者描述的是同一次修改。報告「記憶體減少 66%」時,需要同時說明使用了哪個計數器。

各個計數器能說明什麼

受控堆積的使用量與容量

GetMonoUsedSizeLong 回傳受控堆積使用量,包含等待回收的物件;GetMonoHeapSizeLong 回傳受控堆積容量。Naive 的中位數分別為 8.2 MB 和 10.3 MB,相差 2.1 MB。堆積中有尚未被物件占用的空間,但這兩個中位數之差並不是另外測得的可用容量。

在本次 Mono 後端測試中,GC.GetTotalMemory(false) 與堆積使用量的讀值在顯示精度內一致;其他後端不保證如此。堆積使用量也不能直接說明配置速率或記憶體回收耗時。排查 GC 相關的影格時間問題時,還要在 Profiler 中查看每影格配置量和記憶體回收活動。

Unity 記憶體池的已用量與保留量

GetTotalAllocatedMemoryLong 回傳 Unity 配置器記憶體池中正在使用的量。8.2 MB 的受控堆積讀值,僅相當於 167.2 MB 已配置讀值的約 5%。受控堆積的小幅下降不足以解釋已配置記憶體減少的 110.6 MB。由此可推知,這次模式切換節省的主要是原生引擎記憶體;不過本次測試沒有依物件拆分記憶體用量。

GetTotalReservedMemoryLong 還包含池內的可用容量。Naive 的保留量為 263.4 MB、已用量為 167.2 MB,兩個中位數相差 96.2 MB。InstancedDirect 的差值是 98.6 MB,變化很小。因此,保留量減少 41%、已用量減少 66%,比例看似不同,實際減少量卻很接近:108.2 MB 和 110.6 MB。這裡的差值同樣由中位數相減得出,並非獨立取樣的未使用記憶體。

行程工作集與私人認可量

OS 的工作集統計目前常駐在實體記憶體中的分頁,包含共用分頁,對應工作管理員的 工作集(Working set)。私人認可量統計行程專用的已認可記憶體,包含已換出的部分,對應 認可大小(Commit size)。

Naive 的工作集中位數是 852.4 MB,比已配置記憶體中位數 167.2 MB 高 685.2 MB。這個差值來自定義不同的計數器,並不是某一類記憶體的測量結果。執行檔與 DLL 的對應、圖形驅動程式與外掛程式的記憶體配置,都可能不在 Unity 配置器的追蹤範圍內。本次實驗無法確定它們分別占了多少。

圖形驅動程式記憶體與無法取得的讀值

GetAllocatedMemoryForGraphicsDriver 可在 Editor 和 Development Player 中提供讀值,其原生端測量受 ENABLE_PROFILER 控制。本次 Release Player 回傳 0,兩個 Development 模式則均為 99.0 MB。在這個精度下,沒有觀察到模式間的差異。

Release 中的 0 表示讀值無法取得,不代表驅動程式未使用記憶體。API 未回傳正值時,Framedash 不傳送 mem.vram 鍵。

工作管理員為什麼顯示另一個數字

在工作管理員的 處理程序 索引標籤中,該程式的 記憶體 欄顯示 738.7 MB。同一時刻,外部查詢得到的工作集為 795.5 MB,私人認可量為 1,091.3 MB。顯示欄統計的是私人工作集,也就是常駐在實體記憶體中的行程專用分頁。它不包含共用的常駐分頁,也不包含已換出的專用分頁,因此不同於表中的兩個 OS 計數器。

這些是單一時刻的觀測值,也就不必與表中 120 秒的中位數 852.4 MB 和 1,168.3 MB 相同。比較時,計數器與取樣區間都要一致。

比較修改前後,保持建置類型一致

我們也用 Development 建置測量了這兩個模式。已配置記憶體為 183.6 MB 和 84.6 MB,已保留記憶體為 337.7 MB 和 221.5 MB。四個數字均高於 Release 的結果:已配置 167.2 MB 和 56.6 MB,已保留 263.4 MB 和 155.2 MB。Development 建置包含效能分析所需的檢測程式碼,但僅憑這次比較,無法確定它占了多少增加量。

工作集的變化方向卻不同:Naive 為 792.1 MB,低於 Release;InstancedDirect 為 469.6 MB,高於 Release。配置器使用量上升,不代表常駐分頁量也會同步上升。判斷最佳化效果時,應將 Release 與 Release、Development 與 Development 比較,否則建置組態就成了額外變數。

對比 Release 建置和 Development 建置的兩張圖。左邊是已配置記憶體,Release 是 167.2 和 56.6 MB,Development 是 183.6 和 84.6 MB。右邊是 OS 工作集,Release 是 852.4 和 412.0 MB,Development 是 792.1 和 469.6 MB
兩塊面板的縱軸刻度不同,只能在同一塊面板裡比較。已配置記憶體在 Development 建置裡兩種模式都漲了,分別是 +16.4 MB 和 +28.0 MB;工作集卻沒有跟著同向走,Naive 是 -60.3 MB,InstancedDirect 是 +57.6 MB。

Framedash 的記憶體欄位記錄什麼

傳輸結構只有一個 int64 記憶體欄位 memory_used_bytes,但它的資料來源取決於引擎。儀表板將位元組數除以 1048576 後標為 MB;嚴格來說,這個除數對應 MiB。

  • Unity:Profiler.GetTotalAllocatedMemoryLong(),即本文讀出 167.2 MB 和 56.6 MB 的計數器。SDK 每 10 秒傳送一次 perf_heartbeat,其中包含這個值
  • UE5:FPlatformMemory::GetStats().UsedPhysical,是平台提供的行程實體記憶體統計,與 Unity 的配置器計數器不同
  • Godot(C#):Performance.Monitor.MemoryStatic 回傳正值時使用該值,否則改用 System.GC.GetTotalMemory(false)。匯出的 Release 建置通常會使用這個受控堆積備援值

因此,Godot 的同一欄位可能隨建置組態改變意義。不要直接比較資料來源不同的建置。Unity 的 8.2 MB 受控堆積讀值和 167.2 MB 已配置讀值可以說明這種區別,但不能用作 Godot 的換算比例。

Unity SDK 也會將 GetMonoUsedSizeLong 以 mem.heap、將 GetAllocatedMemoryForGraphicsDriver 以 mem.vram 附加傳送。只有對應 API 回傳正值時才會附帶該鍵,缺少鍵不等於測到了 0 位元組。

開啟遙測的另一次執行

在開啟 SDK 遙測傳送的獨立執行中,GetMonoUsedSizeLong 的 p50 從 Naive 的 8.2 MB 升至 9.8 MB,從 InstancedDirect 的 6.9 MB 升至 8.8 MB。已配置記憶體為 167.0 MB 和 56.6 MB,工作集為 857.3 MB 和 415.3 MB。這些已配置記憶體與工作集的讀值,和關閉遙測時的對應值均相差不到 5 MB。

受控堆積增加的 1.6 MB 和 1.9 MB 是本組態下的觀測結果,不能當作通用的 SDK 額外負擔估計。資料未區分事件緩衝區、序列化和 SDK 記錄各占多少,執行紀錄也無法確認兩次的記錄設定是否一致。

Framedash 的建置比較並排顯示 p50 和 p95。即使中位數變化不大,p95 也可能反映較高讀值的變化,但它不是峰值。可以從 CI 執行 framedash perf-diff,在發布前比較建置。將結果與逐影格基準測試對照之前,先確認百分位數的樣本是什麼。

Framedash 的建置比較畫面,20260828-Naive-memlayers-tel 和 20260828-InstancedDirect-memlayers-tel 兩個 build id 並排顯示,可以看到記憶體的 p50 和 p95 兩個資料行
把兩個 build id 並排放在一起的比較畫面。這是開著遙測上傳測出來的那一次執行,記憶體 p50 從 167.0 MB 到 56.6 MB。這個畫面上的百分位是對每個建置上報的 133 筆事件算的,不是像上面表格那樣按影格算的。

這些結果的適用範圍

本次測量涵蓋 Windows、D3D12、Mono、桌機 GPU 和 1920x1080,未測試 IL2CPP 或其他解析度。工作負載每個影格都會改寫全部 20,000 個位置。其他平台、後端、解析度和工作負載需要重新測量;這裡的大小關係與降幅並非 Unity 的通用比例。

先選計數器,再統一比較條件

只報告「記憶體是 852 MB」,並未說明統計範圍。同一次執行還得到了 8.2 MB 和 1,168.3 MB。應依要回答的問題選擇指標:

  • 檢查 Unity 配置器使用量的變化:比較已配置記憶體;若還要了解記憶體池容量,同時查看已保留記憶體
  • 評估裝置記憶體需求:查看目標平台的行程工作集與認可量,包含峰值。120 秒的中位數不足以證明遊戲能在裝置記憶體中執行
  • 排查記憶體回收問題:結合受控堆積使用量、配置速率與記憶體回收活動分析

前後比較時,保持計數器、平台、指令碼後端、建置類型、解析度、工作負載和遙測設定一致,暖機時間、取樣間隔與彙整區間也要相同。工作管理員的瞬間值、逐影格 p50 和遙測事件的百分位數,即使都顯示為 MB,回答的仍是不同的問題。