初级代码优化和计算成本降低指南(C#、Visual Basic、C++、F#)

缩短计算时间意味着降低成本,因此优化代码可以节省资金。 本文介绍如何使用各种分析工具来帮助你完成此任务。

这里的目的不是提供分步说明,而是向你展示如何有效地使用分析工具以及如何解释数据。 CPU 使用率工具可帮助你捕获和可视化应用程序中使用计算资源的位置。 CPU 使用率视图(如调用树和火焰图)提供了一个良好的图形可视化效果,其中显示了应用程序中所用时间的分布情况。 此外,自动见解可能会显示产生很大影响的精确优化。 其他分析工具还可以帮助你隔离问题。 要比较工具,请参阅应选择哪种工具?

开始调查

  • 通过跟踪 CPU 使用率开始调查。 CPU 使用率工具通常有助于开始性能调查并优化代码以降低成本。
  • 接下来,如果需要其他见解来帮助厘清问题或提高性能,请考虑使用其他分析工具之一收集跟踪。 例如:
    • 查看内存使用情况。 对于 .NET,请先尝试 .NET 对象分配工具。 对于 .NET 或 C++,可以查看内存使用情况工具。
    • 如果应用使用文件 I/O,请使用文件 I/O 工具。
    • 如果使用的是 ADO.NET 或 Entity Framework,可以尝试使用数据库工具来检查 SQL 查询、精确查询时间等。

数据集合示例

本文中显示的示例屏幕截图基于 .NET 应用,该应用针对博客和关联的博客文章的数据库运行查询。 首先检查 CPU 使用率跟踪,以寻找优化代码和降低计算成本的机会。 大致了解所发生的情况后,还将查看其他分析工具的跟踪,以帮助隔离问题。

数据收集需要执行以下步骤(此处未显示):

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

检查高 CPU 使用率的区域

首先使用 CPU 使用率工具收集跟踪。 加载诊断数据时,首先检查显示热门见解和热路径的初始 .diagsession 报表页。 热路径显示应用中 CPU 使用率最高的代码路径。 这些部分可能会提供提示,帮助你快速识别可以改进的性能问题。

还可以在“调用树”视图中查看热路径。 若要打开此视图,请使用报表中的“打开详细信息”链接,然后选择“调用树”。

在此视图中,你将再次看到热路径,其中显示了应用中 GetBlogTitleX 方法的高 CPU 使用率,占应用 CPU 使用率的 60% 左右。 但是,GetBlogTitleX 的“自 CPU”值低,仅为 0.10% 左右。 与“总 CPU”不同,“自 CPU”值不包括其他函数所用的时间,因此我们知道要在“调用树”视图中进一步查找真正的瓶颈。

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

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

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

若要获取可视化的调用树和不同的数据视图,请切换到“火焰图”视图(从与“调用树”相同的列表中选择)。 同样,GetBlogTitleX 方法似乎对应用的大量 CPU 使用率负责(黄色所示)。 对 LINQ DLL 的外部调用显示在 GetBlogTitleX 框下方,它们使用方法的所有 CPU 时间。

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

收集其他数据

通常,其他工具可以提供附加信息来帮助分析和厘清问题。 例如,由于我们标识了 LINQ DLL,因此我们将首先尝试数据库工具。 可以多重选择此工具以及 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"

请注意,你正在此处检索大量列值,这些值可能比你需要的还要多。

若要查看应用的内存使用情况,请使用 .NET 对象分配工具收集跟踪(对于 C++,请改用内存使用情况工具)。 内存跟踪中的“调用树”视图显示热路径,有助于识别高内存使用率的区域。 毫不奇怪,GetBlogTitleX 方法似乎正在生成大量对象! 事实上,超过 900,000 个对象分配。

.NET 对象分配工具中“调用树”视图的屏幕截图。

创建的大多数对象是字符串、对象数组和 Int32。 可以通过检查源代码来查看这些类型的生成方式。

优化代码

是时候查看 GetBlogTitleX 源代码了。 在 .NET 对象分配工具中,右键单击方法,然后选择“转到源文件”。 在 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 查询的一些常见建议,并生成此代码。

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 毫秒,占用应用的 CPU 总数的 37%,比 59% 有了显著改善。

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

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

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

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

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

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

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

迭代

可能需要多次优化,可以继续循环访问代码更改,以查看哪些更改可提高性能并降低计算成本。

后续步骤

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