共用方式為


最佳化程式碼並降低計算成本的初學者指南 (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 使用量最高的程式碼路徑。 這些區段可能會提供一些提示,協助我們快速識別可改善的效能問題。

我們也可以在呼叫樹檢視中檢視最忙碌路徑。 若要開啟此檢視,請使用報告中的 [開啟詳細資料] 連結,然後選取 [呼叫樹]

在此檢視中,我們會再次看到最忙碌路徑,其中顯示應用程式中 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 查詢,並提出此程式碼。

foreach (var x in db.Posts.Where(p => p.Author.Contains("Fred Smith")).Select(b => b.Title).ToList())
{
  Console.WriteLine("Post: " + x);
}

在此程式碼中,我們已進行數個變更,以協助最佳化查詢:

  • 新增 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 效能工具。