案例研究:优化代码和降低计算成本的初学者指南(C#、Visual Basic、C++、F#)

减少计算时间意味着降低成本,因此优化代码可以节省资金。 本案例研究使用性能问题的示例应用程序来演示如何使用分析工具提高效率。 如果要比较分析工具,请参阅 应选择哪种工具?

此案例研究涵盖以下主题:

  • 代码优化的重要性及其对降低计算成本的影响。
  • 如何使用 Visual Studio 分析工具分析应用程序性能。
  • 如何解释这些工具提供的数据,以确定性能瓶颈。
  • 如何应用实际策略来优化代码,侧重于 CPU 使用率、内存分配和数据库交互。

跟随步骤,然后将这些技术应用于自己的应用程序,使其更高效且节省成本。

优化案例研究

本案例研究中检查的示例应用程序是一个 .NET 应用程序,它针对博客和博客文章的数据库运行查询。 它利用适用于 .NET 的常用 ORM(Object-Relational 映射)实体框架来与 SQLite 本地数据库进行交互。 该应用程序的结构是执行大量的查询,模拟一个实际方案,其中可能需要 .NET 应用程序来处理广泛的数据检索任务。 示例应用程序是 Entity Framework 入门示例的修改版本。

示例应用程序的主要性能问题在于它如何管理计算资源并与数据库交互。 应用程序存在性能瓶颈,严重影响了其效率,进而影响了与其运行相关的计算成本。 此问题包括以下症状:

  • 高 CPU 使用率:应用程序可能会以不必要的占用大量 CPU 资源的方式执行低效的计算或处理任务。 这可能会导致响应时间变慢,运营成本增加。

  • 低效内存分配:应用程序有时可能会遇到与内存使用情况和分配相关的问题。 在 .NET 应用中,低效的内存管理可能导致垃圾回收增加,进而会影响应用程序性能。

  • 数据库交互开销:针对数据库执行大量查询的应用程序可能会遇到与数据库交互相关的瓶颈。 这包括低效的查询、过多的数据库调用和实体框架功能的不良使用,所有这些都可能会降低性能。

案例研究旨在通过利用 Visual Studio 的分析工具分析应用程序的性能来解决这些问题。 通过了解应用程序性能的改进位置和方式,开发人员可以实现优化以减少 CPU 使用率、提高内存分配效率、简化数据库交互以及优化资源利用率。 最终目标是提高应用程序的整体性能,使其更高效且经济高效地运行。

挑战

解决示例 .NET 应用程序中的性能问题提出了一些挑战。 这些挑战源于诊断性能瓶颈的复杂性。 修复所描述问题的主要挑战如下:

  • 诊断性能瓶颈:主要挑战之一是准确识别性能问题的根本原因。 CPU 使用率高、内存分配效率低下和数据库交互开销可能有多个因素。 开发人员必须有效地使用分析工具来诊断这些问题,这需要对这些工具的工作原理以及如何解释其输出有一定的了解。

  • 知识和资源约束:最后,团队可能面临与知识、专业知识和资源相关的约束。 分析和优化应用程序需要特定的技能和经验,并非所有团队都可以立即访问这些资源。

应对这些挑战需要一种战略方法,该方法结合了分析工具、技术知识以及精心规划和测试的有效使用。 案例研究旨在指导开发人员完成此过程,提供策略和见解,以克服这些挑战并提高应用程序的性能。

策略

下面是本案例研究中方法的高级视图:

  • 我们通过进行 CPU 使用情况跟踪来启动调查。 Visual Studio CPU 使用率工具 通常有助于开始性能调查并优化代码以降低成本。
  • 接下来,为了获取其他见解以帮助隔离问题或提高性能,我们使用另一个分析工具获取跟踪数据。 例如:
    • 我们来看看内存使用情况。 对于 .NET,我们首先尝试 .NET 对象分配工具。 (对于 .NET 或 C++,你可以查看内存使用工具。)
    • 对于 ADO.NET 或实体框架,可以使用 数据库工具 来检查 SQL 查询、精确的查询时间等。

数据收集需要以下任务:

  • 将应用设置为发布版本。
  • 在性能分析器中选择 CPU 使用率工具(Alt+F2)。 (后续步骤涉及其他一些工具。
  • 在性能探查器中,启动应用并收集跟踪。

检查 CPU 使用率较高的区域

使用 CPU 使用情况工具收集跟踪并将其加载到 Visual Studio 后,我们首先检查显示汇总数据的初始 .diagsession 报表页。 使用报表中的“打开详细信息”链接

CPU 使用情况工具中“打开详细信息”的屏幕截图。

在报表详细信息视图中,打开“调用树”视图。 应用中 CPU 使用率最高的代码路径称为 热路径。 热路径火焰图标(显示“热路径”图标的屏幕截图。)可帮助快速识别可能改进的性能问题。

在“调用树”视图中,可以看到应用中 GetBlogTitleX 方法的 CPU 使用率较高,占应用 CPU 使用率的 60% 左右。 但是,GetBlogTitleX 的“自身 CPU”值低,仅为 0.10% 左右。 与 总 CPU不同,自身 CPU 值排除了其他函数消耗的时间,因此我们知道要更深入地查看调用树中的实际瓶颈。

CPU 使用率工具中“调用树”视图的屏幕截图。

GetBlogTitleX 对两个 LINQ DLL 进行外部调用,这两个 LINQ DLL 使用了大部分 CPU 时间,非常高的“自身 CPU”值就证明了这一点。 这是 LINQ 查询可能是要优化的区域的第一个线索。

CPU 使用率工具中“调用树”视图的屏幕截图,其中突出显示了“自身 CPU”。

若要获取可视化的调用树和数据的不同视图,请打开 火焰图 视图。 (或者,右键单击 GetBlogTitleX,然后选择“在火焰图中查看”)。同样,GetBlogTitleX 方法似乎占了应用的 CPU 使用率的大头(黄色所示)。 对 LINQ DLLs 的外部调用显示在 GetBlogTitleX 框下,它们正在占用该方法的所有 CPU 时间。

CPU 使用情况工具中火焰图视图的屏幕截图。

收集其他数据

通常,其他工具可以提供其他信息来帮助分析和隔离问题。 在本案例研究中,我们采用以下方法:

  • 首先,查看内存使用情况。 高 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 的关联字符串以及文章中包含的属性的值,例如博客 ID。

我们稍做一些研究,并找到有关如何优化 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% 有了显著改善。

CPU 使用率工具的“调用树”视图中改进的 CPU 使用率的屏幕截图。

切换到 火焰图 视图以查看显示改进效果的另一个可视化效果。 在此视图中,GetBlogTitleX 还使用较小的 CPU 部分。

CPU 使用率工具的“火焰图”视图中改进的 CPU 使用率的屏幕截图。

在数据库工具跟踪中检查结果,使用此查询仅读取了两条记录,而不是十万条! 此外,查询得到了大大简化,并消除了之前生成的不必要的 LEFT JOIN。

数据库工具中更快查询时间的屏幕截图。

接下来,我们在 .NET 对象分配工具中重新检查结果,并看到 GetBlogTitleX 只负责 56,000 个对象分配,比 900,000 个减少了近 95%%!

.NET 对象分配工具中内存分配减少的屏幕截图。

迭代

可能需要进行多次优化,我们可以继续通过代码更改进行迭代,以查看哪些更改能够提高性能并帮助降低计算成本。

后续步骤

以下文章和博客文章提供了详细信息,可帮助你了解如何有效地使用 Visual Studio 性能工具。