案例研究:將程式代碼優化並降低計算成本的初學者指南(C#、Visual Basic、C++、F#)
降低計算時間表示降低成本,因此優化程式代碼可以節省成本。 此案例研究會使用具有效能問題的範例應用程式來示範如何使用分析工具來提高效率。 如果您想要比較分析工具,請參閱 我應該選擇哪一個工具?
此案例研究涵蓋下列主題:
- 程式代碼優化的重要性及其對降低計算成本的影響。
- 如何使用 Visual Studio 分析工具來分析應用程式效能。
- 如何解譯這些工具提供的數據,以識別效能瓶頸。
- 如何套用實際策略來優化程式代碼,著重於CPU使用量、記憶體配置和資料庫互動。
遵循這些技術,然後將這些技術套用至您自己的應用程式,使其更有效率且符合成本效益。
優化案例研究
此案例研究中檢查的範例應用程式是 .NET 應用程式,會針對部落格和部落格文章的資料庫執行查詢。 它利用 Entity Framework 這個著名的適用於 .NET 的 ORM(用於實現Object-Relational 資料映射)與 SQLite 本機資料庫互動。 應用程式的結構是用來執行大量查詢,並模擬實際案例,其中可能需要 .NET 應用程式來處理大量數據擷取工作。 範例應用程式是 Entity Framework 入門範例的修改版本,。
範例應用程式的主要效能問題在於如何管理計算資源,並與資料庫互動。 應用程式已經遇到效能瓶頸,大幅影響其運行效率,因此增加了與執行它相關的計算成本。 問題包括下列徵兆:
高 CPU 使用量:應用程式可能會以不必要地取用大量 CPU 資源的方式執行效率不佳的計算或處理工作。 這可能會導致回應時間變慢,並增加營運成本。
效率不佳的記憶體配置:應用程式有時會面臨與記憶體使用量和配置相關的問題。 在 .NET 應用程式中,效率不佳的記憶體管理可能會導致垃圾收集增加,進而影響應用程式效能。
資料庫互動額外負荷:對資料庫執行大量查詢的應用程式可能會遇到與資料庫互動相關的瓶頸。 這包括效率不佳的查詢、過多的資料庫呼叫,以及使用 Entity Framework 功能不佳,這一切都可能會降低效能。
案例研究旨在藉由採用 Visual Studio 的分析工具來分析應用程式的效能來解決這些問題。 藉由瞭解應用程式效能的改善位置與方式,開發人員可以實作優化以減少 CPU 使用量、改善記憶體配置效率、簡化資料庫互動,以及優化資源使用率。 最終目標是增強應用程序的整體效能,使其更有效率且符合成本效益來執行。
挑戰
解決範例 .NET 應用程式中的效能問題,會面臨數個挑戰。 這些挑戰源於診斷效能瓶頸的複雜性。 修正所描述問題的主要挑戰如下:
診斷效能瓶頸:其中一個主要挑戰是準確識別效能問題的根本原因。 高 CPU 使用量、記憶體配置效率低,以及資料庫互動額外負荷可能會有多個因素。 開發人員必須使用分析工具有效地診斷這些問題,這需要了解這些工具的運作方式,以及如何解譯其輸出。
知識與資源限制:最後,小組可能會面臨與知識、專業知識和資源相關的限制。 分析及優化應用程式需要特定的技能和經驗,並非所有小組都可以立即存取這些資源。
解決這些挑戰需要一種策略性方法,結合對分析工具、技術知識的有效使用,以及仔細的規劃和測試。 案例研究旨在引導開發人員完成此程式,提供策略和見解來克服這些挑戰並改善應用程式的效能。
策略
以下是此案例研究中方法的高階檢視:
- 我們會採取 CPU 使用量追蹤來開始調查。 Visual Studio CPU 使用量工具 通常有助於開始效能調查,並優化程式代碼以降低成本。
- 接下來,若要取得其他深入解析,以協助找出問題或改善效能,我們會使用其他其中一個分析工具收集追蹤。 例如:
- 我們來看看記憶體使用量。 針對 .NET,我們會先嘗試 .NET 物件配置工具。 (針對 .NET 或 C++,您可以改為查看記憶體使用量工具。
- 針對 ADO.NET 或 Entity Framework,我們可以使用 Database 工具 來檢查 SQL 查詢、精確的查詢時間等等。
資料收集需要下列工作:
- 將應用程式設定為發行版本。
- 在 [效能剖析器] 中選取 [CPU 使用量] 工具 (Alt+F2)。 (後續步驟涉及一些其他工具。
- 從效能分析器啟動應用程式並收集追蹤數據。
檢查高 CPU 使用量的區域
使用 CPU 使用量工具收集追蹤並將其載入 Visual Studio 之後,我們會先檢查初始 .diagsession 報表頁面,其中顯示摘要數據。 使用報表中的 開啟詳細數據 連結。
在報告詳細檢視中,開啟 [呼叫樹狀檢視]。 應用程式中 CPU 使用量最高的程式代碼路徑稱為 熱點路徑。 經常性路徑火焰圖示 ()可協助快速識別可能改善的效能問題。
在 呼叫樹狀結構 檢視中,您可以看到應用程式中 GetBlogTitleX
方法的高 CPU 使用量,佔整個應用程式 CPU 使用量的約 60%。 不過,GetBlogTitleX
的 自我 CPU 值為低,只有大約 .10%。 不同於 CPU 總,自身 CPU 值會排除其他函式所耗費的時間,因此我們知道要在呼叫樹中更深入地查看以找到實際瓶頸。
GetBlogTitleX
對兩個 LINQ DLL 進行外部呼叫,這些 DLL 占用了大部分的 CPU 時間,這可以從非常高的 自用 CPU 值看出。 這是 LINQ 查詢可能是要優化的區域的第一個線索。
若要取得可視化的呼叫樹狀結構和數據的不同檢視,請開啟 Flame Graph 檢視。 (或者,以滑鼠右鍵按兩下 [GetBlogTitleX
],然後選擇 [火焰圖形] 中的 [檢視]。在這裡,看起來 GetBlogTitleX
方法會負責許多應用程式的CPU使用量(如黃色所示)。 對 LINQ DLL 的外部呼叫會顯示在 [GetBlogTitleX
] 方塊下方,而且它們會針對 方法使用所有 CPU 時間。
收集其他數據
其他工具通常可以提供其他資訊來協助分析並隔離問題。 在此案例研究中,我們會採用下列方法:
- 首先,查看記憶體使用量。 高 CPU 使用量與高記憶體使用量之間可能存在關聯,因此查看這兩者有助於識別來源問題。
- 因為我們識別出 LINQ DLL,所以我們也會查看資料庫工具。
檢查記憶體使用量
若要查看應用程式在記憶體使用量方面的進展,我們會使用 .NET 物件配置工具收集追蹤(針對C++,您可以改用記憶體使用量工具)。 記憶體追蹤中的 呼叫樹狀結構 檢視會顯示熱門路徑,並協助我們辨識高記憶體使用量。 在這個階段,毫無意外,GetBlogTitleX
方法似乎正在產生大量的物件! 事實上,超過 900,000 個物件分配。
建立的大部分物件都是字串、物件陣列和 Int32。 我們或許可以藉由檢查原始程式碼來查看這些型別的產生方式。
檢查資料庫工具中的查詢
在 [效能分析工具] 中,我們選取 [資料庫] 工具,而不是 [CPU 使用量] (或選取兩者)。 當我們收集追蹤檔後,請在診斷頁面開啟 [查詢] 標籤。 在 [資料庫追蹤的查詢] 索引標籤中,您可以看到第一個數據列顯示最長的查詢,2446 毫秒。 記錄 欄顯示查詢讀取的記錄數目。 您可以使用這項資訊進行稍後的比較。
藉由檢查 [查詢] 數據行中 LINQ 所產生的 SELECT
語句,我們會將第一個數據列識別為與 GetBlogTitleX
方法相關聯的查詢。 若要檢視完整的查詢字串,請展開數據行寬度。 完整的查詢字串為:
SELECT "b"."Url", "b"."BlogId", "p"."PostId", "p"."Author", "p"."BlogId", "p"."Content", "p"."Date", "p"."MetaData", "p"."Title"
FROM "Blogs" AS "b" LEFT JOIN "Posts" AS "p" ON "b"."BlogId" = "p"."BlogId" ORDER BY "b"."BlogId"
請注意,應用程式在這裡擷取許多數據行值,或許比我們需要的更多。 讓我們看看原始程式碼。
優化程序代碼
是時候看看 GetBlogTitleX
原始程式碼了。 在 [資料庫] 工具中,以滑鼠右鍵按下查詢,然後選擇 移至來源檔案。 在 GetBlogTitleX
的原始程式碼中,我們發現下列程式代碼會使用LINQ讀取資料庫。
foreach (var blog in db.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
{
foreach (var post in blog.Posts)
{
if (post.Author == "Fred Smith")
{
Console.WriteLine($"Post: {post.Title}");
}
}
}
此程式代碼會使用 foreach
迴圈搜尋資料庫中由「Fred Smith」作為作者的任何部落格。 查看它時,您可以看到記憶體中會產生許多物件:資料庫中每個部落格的新物件陣列、每個URL的相關字串,以及文章中包含的屬性值,例如部落格標識符。
我們做一些研究,並尋找一些常見的建議,以瞭解如何優化 LINQ 查詢。 或者,我們可以節省時間,讓 科皮洛特 為我們做研究。
如果我們使用 Copilot,我們會從內容功能表選取 詢問 Copilot,然後輸入下列問題:
Can you make the LINQ query in this method faster?
提示
您可以使用像 /optimize 這樣的斜線命令,來幫助形成適合 Copilot 的良好問題。
在此範例中,Copilot 會提供下列建議的程式代碼變更,以及說明。
public void GetBlogTitleX()
{
var posts = db.Posts
.Where(post => post.Author == "Fred Smith")
.Select(post => post.Title)
.ToList();
foreach (var postTitle in posts)
{
Console.WriteLine($"Post: {postTitle}");
}
}
此程式代碼包含數項變更,可協助優化查詢:
- 已新增
Where
子句,並排除其中一個foreach
迴圈。 - 投影只有
Select
語句中的 Title 屬性,這是我們在此範例中所需要的一切。
接下來,我們會使用分析工具重新測試。
結果
更新程式代碼之後,我們會重新執行CPU使用量工具來收集追蹤。
呼叫樹狀結構 檢視顯示,GetBlogTitleX
只執行 1754 毫秒,使用應用程式 CPU 總量的 37%,相較於之前的 59%是大幅改善。
切換至 Flame Graph 視圖,以查看另一個顯示變化的可視化圖。 在此檢視中,GetBlogTitleX
也會使用較小的CPU部分。
檢查資料庫工具追蹤中的結果,而且只會使用此查詢讀取兩筆記錄,而不是 100,000! 此外,查詢會大幅簡化,並移除了先前生成的不必要的 LEFT JOIN。
接下來,我們會在 .NET 物件配置工具中重新檢查結果,並查看 GetBlogTitleX
只負責 56,000 個物件配置,幾乎比 900,000 減少近 95 個%!
重複
可能需要多次優化,我們可以透過程式碼調整進行反覆測試,以了解哪些變更能改善效能,進而降低計算成本。
後續步驟
下列文章和部落格文章提供詳細資訊,可協助您瞭解如何有效地使用 Visual Studio 效能工具。
- 案例研究:釐清效能問題
- 案例研究:30分鐘內性能加倍
- 使用新的檢測工具 改善Visual Studio效能