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

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

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

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

跟随操作,然后将这些技术应用于自己的应用程序,使其更经济高效。

优化案例研究

本案例研究中检查的示例应用程序是一个 .NET 应用程序,可针对博客和博客文章数据库运行查询。 它利用了实体框架,这是一种用于 .NET 的常用 ORM(对象关系映射),以与 SQLite 本地数据库交互。 该应用程序的结构可以执行大量查询,模拟了需要 .NET 应用程序处理大量数据检索任务的真实方案。 示例应用程序是实体框架入门示例的修改版本。

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

  • 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 DLL 的外部调用显示在 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,我们从上下文菜单中选择询问 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 使用率的屏幕截图。

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

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

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

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

迭代

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

后续步骤

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