初级代码优化和计算成本降低指南(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 使用率工具中“火焰图”视图的屏幕截图。

收集其他数据

通常,其他工具可以提供附加信息来帮助分析和厘清问题。 对于此示例,我们采用以下方法:

  • 首先,我们来看看内存使用情况。 高 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 查询的一些常见建议,并生成此代码。

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 性能工具。