分享方式:


案例研究:最佳化程式碼並降低計算成本的初學者指南 (C#、Visual Basic、C++、F#)

減少計算時間表示降低成本,因此最佳化程式碼可以節省金錢。 本案例研究使用具有效能問題的範例應用程式,來示範如何使用分析工具提高效率。 如果要比較分析工具,請參閱我應該選擇哪一個工具?

本案例研究涵蓋以下主題:

  • 程式碼最佳化的重要性,及其對降低計算成本的影響。
  • 如何使用 Visual Studio 分析工具來分析應用程式效能。
  • 如何解釋這些工具提供的資料,找出效能瓶頸。
  • 如何套用實用策略來最佳化程式碼,著重於 CPU 使用量、記憶體配置和資料庫互動。

遵循這些技術,然後將其套用到您自己的應用程式中,使其更有效率且符合成本效益。

最佳化案例研究

本案例研究中檢驗的範例應用程式是一個 .NET 應用程式,其會對部落格和部落格文章的資料庫執行查詢。 它利用 Entity Framework (適用於 .NET 的熱門 ORM (物件關聯對應)) 與 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,我們可以使用資料庫工具來檢查 SQL 查詢、精確的查詢時間等。

資料收集需要以下工作:

  • 將應用程式設定為發行組建。
  • 從效能分析工具中選取 CPU 使用量工具 (Alt+F2)。 (後續步驟涉及一些其他工具。)
  • 從效能分析工具中,啟動應用程式並收集追蹤。

檢查高 CPU 使用量的區域

使用 CPU 使用量工具收集追蹤資料並將其載入到 Visual Studio 後,我們先檢查顯示摘要資料的初始 .diagsession 報告頁面。 使用報告中的開啟詳細資料連結。

在 CPU 使用量工具中開啟詳細資訊的螢幕擷取畫面。

在報告詳細資料檢視中,開啟「呼叫樹狀圖」檢視。 應用程式中 CPU 使用量最高的程式碼路徑稱為「最忙碌路徑」。 最忙碌路徑火焰圖示 (顯示「最忙路徑」圖示的螢幕擷取畫面。) 可協助快速識別可以改進的效能問題。

在「呼叫樹狀圖」檢視中,您可以看到應用程式中 GetBlogTitleX 方法的 CPU 使用量很高,約佔應用程式 CPU 使用量的 60%。 不過,GetBlogTitleX 的 [自我 CPU] 值很低,只有大約 .10%。 不同於「CPU 總計」,「自我 CPU」值會排除其他函式所花費的時間,因此我們知道要更深入地查看呼叫樹狀圖,以取得實際瓶頸。

CPU 使用量工具中 [呼叫樹狀結構] 檢視的螢幕擷取畫面。

GetBlogTitleX 對使用大部分 CPU 時間的兩個 LINQ DLL 進行外部呼叫,非常高的 [自我 CPU] 值證明了這一點。。 這是第一個線索,表明 LINQ 查詢可能是需要最佳化的領域。

CPU 使用量工具中 [呼叫樹狀結構] 檢視的螢幕擷取畫面,其中反白顯示了 Self CPU。

若要取得視覺化的呼叫樹狀圖和其他資料檢視,請開啟「火焰圖」檢視。 )或者,以右鍵點擊 GetBlogTitleX 並選擇「在火焰圖中查看」。) 同樣地,看起來 GetBlogTitleX 方法佔用了應用程式的大量 CPU 使用量 (以黃色顯示)。 對 LINQ DLL 的外部呼叫會顯示在 GetBlogTitleX 方塊下方,而且其正在使用該方法的所有 CPU 時間。

CPU 使用量工具中 [Flame Graph] 檢視的螢幕擷取畫面。

收集供額資料

其他工具通常可以提供額外資訊來協助分析並隔離問題。 在本案例研究中,我們採用以下方法:

  • 首先,查看記憶體使用情況。 高 CPU 使用量與高記憶體使用量之間可能有關聯性,因此查看這兩者有助於找出問題。
  • 因為我們識別出 LINQ DLL,所以我們也會了解資料庫工具。

檢查記憶體使用量

若要查看應用程式在記憶體使用量方面發生什麼情況,請使用 .NET 物件配置工具收集追蹤資料 (若為 C++,您可以改用記憶體使用量工具)。 記憶體追蹤中的呼叫樹檢視會顯示最忙碌路徑,並協助我們識別高記憶體使用量的區域。 此時不要驚訝,GetBlogTitleX 方法似乎會產生許多物件! 事實上,超過 900,000 個物件配置。

.NET 物件配置工具中 [呼叫樹狀結構] 檢視的螢幕擷取畫面。

建立的物件多半是字串、物件陣列和 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,我們會從操作功能表中選取 [詢問 Copilot ],然後輸入下列問題:

Can you make the LINQ query in this method faster?

提示

您可以使用 slash 命令,例如 /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 毫秒,使用 37% 的應用程式 CPU 總量,比起 59% 有了大幅改善。

CPU 使用量工具的 [呼叫樹狀結構] 檢視中改善的 CPU 使用量的螢幕擷取畫面。

切換至火焰圖檢視,以查看另一個顯示改善的視覺化效果。 在此檢視中,GetBlogTitleX 也使用了較小部分的 CPU。

CPU 使用量工具的 [Flame Graph] 檢視中改善的 CPU 使用量的螢幕擷取畫面。

檢查資料庫工具追蹤中的結果,使用此查詢只讀取兩筆記錄,而不是 100,000 筆! 此外,查詢會大幅簡化,並消除先前產生的不必要 LEFT JOIN。

資料庫工具中更快查詢時間的螢幕擷取畫面。

接下來,我們重新檢查 .NET 物件配置工具中的結果,並看到 GetBlogTitleX 只負責 56,000 個物件配置,比 900,000 個減少了近 95%!

.NET 物件配置工具中減少的記憶體配置的螢幕擷取畫面。

反覆執行

可能需要多個最佳化,而且我們可以繼續逐一查看程式碼變更,以查看哪些變更可改善效能並協助降低計算成本。

下一步

下列文章和部落格文章提供詳細資訊,可協助您了解如何有效地使用 Visual Studio 效能工具。