測量程式碼效能並建立基準

已完成

測量程式碼效能並建立基準是最佳化過程中的關鍵步驟。 它提供了一個參考點來評估您所做的任何更改的結果。

建立績效基準

效能基準是一組測量值,可在您進行任何變更之前擷取程式碼的執行方式。 這是你的參考點。 如果沒有比較基準,您不知道您的更新是提高了程式碼效能還是使事情變得更糟。

以下是在基線中衡量的一些關鍵方面:

  • 執行時間(延遲): 執行某個操作或流程需要多長時間? 此值可以針對小型函式 (例如,排序 10,000 個項目需要 5 毫秒) 或端對端案例 (例如,服務在 250 毫秒內回應請求) 來測量。

  • 吞吐量: 在給定的時間內可以執行多少次操作? 此值與伺服器應用程式(每秒請求數)、批次處理(每分鐘處理的記錄數)等相關。

  • 資源使用情況: 記憶體耗用量 (工作集、配置、記憶體回收頻率)、CPU 使用率 (是否將核心固定在 100%? 持續多久?)、磁碟 I/O、網路 I/O 等,視程式碼的作用而定。

  • 可擴展性特性: 上述指標如何隨著輸入大小的增加或負載(並發使用者)的增加而變化? 您可能會測量到一個函式處理 100 個項目需要 0.1 毫秒,處理 10,000 個項目需要 10 毫秒。 這表示它如何縮放(在本例中,與項目數量大致呈線性)。 這種基準可協助您了解可擴展性並設定目標。 例如,也許您希望 100k 個項目在 200 毫秒內排序,這可能意味著您需要更有效率的演算法或平行排序。

建立基準時:

  • 使環境盡可能一致。 在同一台機器上,在類似的條件下運行測試(關閉後台程式、使用發布版本等)。 性能可能會因運行而異;最大限度地減少噪音可提供更可靠的數據。

  • 使用發行組建和實際設定。 偵錯組建通常因為額外的檢查和缺乏最佳化而執行速度較慢。 如果您要測量演算法速度,請在開啟最佳化的情況下以發行模式進行編譯。 同樣地,如果您的程式碼是多執行緒或非同步的,請在類似生產環境的案例中進行測試 (例如,相同數目的執行緒或相同的資料集大小)。

  • 自動化測量或編寫腳本。 您應該多次執行程式碼。 擁有一個簡單的基準載入器或使用能將其自動化的工具會有所幫助。 例如,對於一個小型函數,您可以編寫一個循環來呼叫它、測量時間並計算平均值。

  • 記錄數字。 記錄基準指標(時間、記憶體等)。 如果您有列印效能資訊的單元測試或記錄輸出,請儲存該輸出。 對於非平凡的情況,使用電子表格或簡單表格來比較「之前與之後」很有用。

使用秒錶進行基準測量

C# 提供了該 System.Diagnostics.Stopwatch 類,它對於測量程式碼區塊的執行時間很有用。

例如,假設您有下列方法 ProcessData(),您懷疑它速度緩慢:

var sw = Stopwatch.StartNew();
ProcessData();
sw.Stop();
Console.WriteLine($"ProcessData() completed in {sw.ElapsedMilliseconds} milliseconds");

想像一下,此程式碼始終輸出大約 120 毫秒的值。 該平均值是您使用的測試條件下的 ProcessData() 基準。

記憶體使用量的基準

GC.GetTotalMemory 方法是 .NET 方法呼叫,可擷取記憶體回收行程 (GC) 所配置的目前記憶體數量。 這個 GetTotalMemory 方法會在叫用時擷取目前的記憶體配置狀態。

在作業前後檢查GC.GetTotalMemory(false)以記錄記憶體的使用情形。

例如,假設您有以下程式碼:

long memoryBefore = GC.GetTotalMemory(forceFullCollection: true);
ProcessData();
long memoryAfter = GC.GetTotalMemory(forceFullCollection: true);
Console.WriteLine($"Memory used by ProcessData(): {memoryAfter - memoryBefore} bytes");

如果這段程式碼列印「ProcessData() 使用的記憶體:50,000 位元組」,那麼這就是操作分配的淨記憶體量。 此值可以作為您的基準的一部分,特別是在您擔心記憶體或垃圾回收負擔時。

情境基準

基線通常不是一個數字。 您可能有特定規模的基準,然後在更大的規模上進行測試。 例如:

  • 排序 10k 項目需要 50 毫秒(基準為 10k)。
  • 對 100k 個項目進行排序需要 800 毫秒。

此結果表示效能會隨著輸入的成長而降低,這是預期的。 它還表明該算法可能比 O(n log n) 更差,因為將項目增加 10 倍似乎會將時間增加 16 倍。 這種類型的基準可協助您了解可擴展性並設定目標。

績效測量工具和技術

了解效能需要測量和分析演算法特徵。 在深入研究測量工具之前,了解關鍵效能概念非常重要:

了解演算法複雜性

時間複雜度 描述了演算法的運行時間如何隨著輸入大小的增加而增長,通常使用 Big O 表示法表示:

  • O(1) - 常數時間:效能不會隨著輸入大小而改變 (例如字典查閱)。
  • O(n) - 線性時間:效能會隨著輸入成比例成長 (例如,逐一查看清單一次)。
  • O(n²) - 二次時間:效能隨著輸入大小的平方而成長 (例如,巢狀迴圈)。
  • O(log n) - 對數時間:效能會隨著輸入的增加而緩慢成長 (例如,二分搜尋)。

空間複雜度 使用相同的 Big O 表示法來衡量記憶體使用量如何隨著輸入大小而成長。 了解這些概念有助於確定為什麼某些程式碼模式會成為大規模瓶頸。

識別效能模式和反模式

常見的效能反模式包括:

  • N+1查詢問題:做一個查詢得到一個列表,然後再做N個查詢相關資料。
  • 效的資料結構:使用清單進行頻繁查找,而不是字典或雜湊集。
  • 字串串連過早:使用而不是+=來建置大型字串。
  • 同步作業:當非同步作業會更好時,封鎖具有同步 I/O 的執行緒。

快取策略 可以透過將經常存取的資料儲存在記憶體中來顯著提高效能,避免昂貴的重新計算或 I/O 操作。

從簡單到複雜,以下是衡量效能的工具和方法:

使用秒錶手動計時

手動檢測具有 Stopwatch 甚至 DateTime.UtcNow 差異的程式碼可提供快速的深入解析。 這種方法屬於臨時性,但容易理解,通常能夠滿足初步調查的需求。 您可以在程式碼區段周圍散佈計時記錄 (例如,記錄資料庫查詢所花費的時間、檔案剖析所花費的時間等)。

記錄和計數器

記錄是臨機操作,但可存取,而且通常足以進行初次調查。 您可以在程式碼區段周圍散佈計時記錄 (例如,記錄資料庫查詢所花費的時間、檔案剖析所花費的時間等)。

事件記錄和效能指標

新增策略性日誌記錄可以揭示效能模式並幫助識別瓶頸:

  • 作業計數:記錄已處理的項目數量、已執行的資料庫查詢,或快取命中或快取未命中的情況。
  • 計時細分:測量操作的不同階段(例如,「資料庫查詢:50毫秒,處理:20毫秒,序列化:10毫秒」)。
  • 資源利用率:追蹤記憶體分配、執行緒池使用情況或連線池指標。
  • 效能指標:監控延遲 (回應時間)、輸送量 (每秒作業) 和可擴展性特性。

例如,如果您的日誌顯示「已從資料庫擷取 1000 筆記錄」,而您預期會看到 100 筆記錄,則差異可能表示 N+1 查詢問題或效率低下的查詢邏輯。 同樣地,記錄快取命中率可以幫助評估快取策略的有效性。

內建分析器(Visual Studio 診斷工具)

Visual Studio(企業版,在某種程度上是社群版)具有分析工具(CPU 使用率、記憶體使用率、效能分析器)。 您可以在分析器下執行應用程式,例如,它會顯示依函式劃分的 CPU 時間明細,或堆積上的物件清單,以及分配它們的人員。

  • CPU 分析器通常會產生呼叫樹狀結構,您可以在其中查看哪些方法耗用了最多的 CPU 時間。
  • 記憶體分析器可以拍攝快照集,以顯示記憶體使用量的成長情況,以及哪些類型的物件佔用空間。

使用分析工具通常很簡單,只需按一下 [開始診斷] 並選擇設定檔類型。

.NET CLI 工具

針對 .NET Core 和 .NET 5+,Microsoft 提供命令列工具,例如 dotnet-countersdotnet-tracedotnet-dumpdotnet-gcdump

  • dotnet-counters 可以顯示正在運行的應用程式的即時效能指標(GC 集合、異常、執行緒池使用情況等)。
  • dotnet-trace 可以收集應用程式執行的追蹤,可以分析這些追蹤以查看哪些方法正在運行。

這些工具更進階,但對於您無法附加 GUI 分析工具的深入探索或生產分析而言相當寶貴。

微基準測試程式庫

如果您想要以嚴格的方式比較函式的兩個實作 (例如,原始與建議的最佳化版本) ,BenchmarkDotNet 是熱門的程式庫。 它多次運行函數,預熱即時編譯器,精確測量,並提供統計數據(如平均值、標準差)。 這些數據用於高精度的微基準測試(小型隔離代碼路徑)。

效能/負載測試

對於較大的案例 (Web 應用程式、服務),您可以撰寫負載測試或使用工具 (例如 JMeter、k6 或 Visual Studio 負載測試) 來模擬許多要求或大型輸入。 這種方法可以揭示壓力下的輸送量和穩定性,並幫助識別僅在大規模上出現的瓶頸。

系統監視

監控整體系統行為,以識別資源瓶頸和可擴展性問題:

  • CPU 使用模式:高 CPU 使用率可能表示演算法效率低下或運算瓶頸。
  • 記憶體消耗:記憶體使用量的增加可能表示記憶體洩漏、資料結構效率低下或物件分配過多。
  • I/O 指標:磁碟或網路 I/O 過高可能表示資料存取模式效率低下或快取不佳。
  • 垃圾回收:頻繁的垃圾回收會影響效能,尤其是在高吞吐量應用程式中。

Windows 上的任務管理器或 dotnet-countersPerfMon 等工具可以提供系統級見解。 了解這些指標有助於將程式碼層級效能與系統資源利用率相關聯。

監控改進並避免退化

建立基準後,您可以開始實施程式碼改進。 程式碼改進應該在程式碼更新週期中進行,然後進行測量,以查看變更是否具有預期的效果。

當您進行變更時,請務必使用與基準線相同的方法重新測量。 將新測量值與基準線進行比較。

以下是此過程的一些建議指南:

  • 一次更改一件事(如果可能的話):所以你知道是什麼導致了任何改善或倒退。

  • 執行與基準相同的測試/測量: 使用相同的程序並直接比較指標。

  • 如果實現改進: 太好了,考慮一下它是否達到了目標。 如果您需要 2 倍的速度提升,但只達到了 1.5 倍,您可能需要再進行進一步的迭代。

  • 如果沒有改善或效能較差: 調查原因。 可能是您的變更沒有解決真正的瓶頸,或將一種成本換成另一種成本。

  • 檢查副作用:副作用可能包括非預期的正確性或效能問題。 例如,您可能會最佳化 CPU 效能,但會看到記憶體使用量大幅增加。 這種副作用可以接受嗎?

  • 自動化迴歸偵測: 撰寫在變更前後執行的效能單元測試或基準測試是迴歸偵測的常用技術。 雖然並非每個團隊都編寫效能測試,但擁有一小套效能測試(尤其是針對關鍵路徑)並不是一個壞主意,以確保新的變更不會大幅減慢速度。

使用 GitHub Copilot 協助測量

GitHub Copilot 可協助您設定效能測量技術:

  • 詢問計時範例:「如何測量 C# 方法的執行時間?」GitHub Copilot 可以使用範例程式碼提出建議 Stopwatch
  • 取得分析指引:「我可以使用哪些工具來分析 .NET 應用程式?」GitHub Copilot 可以列出分析工具及其使用案例。
  • 生成基準代碼: GitHub Copilot 可以幫助創建 BenchmarkDotNet 類或其他測量工具。

GitHub Copilot 可作為正確實施測量技術的快速參考。

總結

測量程式碼效能並建立基準是最佳化過程中的關鍵步驟。 透過系統地擷取執行時間、資源使用情況和可擴展性特性,您可以建立參考點來評估變更的影響。 利用各種測量技術(從簡單的計時 Stopwatch 到進階分析工具),使您能夠準確識別瓶頸。 始終確保效能改進不會損害正確性或引入新問題。 透過嚴格的測量和迭代方法,您可以有效地增強程式碼的效能,同時保持其完整性。