案例研究:隔离性能问题(C#、Visual Basic、F#)

使用分析工具调查性能问题并隔离问题区域。 本案例研究使用性能问题的示例应用程序来演示如何使用分析工具提高效率。 如果要比较分析工具,请参阅我应选择哪种工具?

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

  • 如何使用 Visual Studio 分析工具分析应用程序性能。
  • 如何解释这些工具提供的数据,以确定性能瓶颈。
  • 如何应用实际策略来优化代码,重点关注 .NET 计数器、调用计数和计时数据。

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

隔离性能问题案例研究

本案例研究中的示例应用程序是一个 ASP.NET 应用,对模拟数据库运行查询。 此示例基于诊断示例

示例应用程序的主要性能问题在于低效的编码模式。 应用程序存在性能瓶颈,严重影响其效率。 此问题包括以下症状:

  • CPU 使用率低:应用程序显示 CPU 使用率低,表明 CPU 不是瓶颈。

  • 高线程池线程计数:线程计数相对较高且稳步上升,表明线程池不足。

  • 应用程序响应缓慢:由于缺少处理新工作项的可用线程,应用程序响应缓慢。

案例研究旨在通过利用 Visual Studio 的分析工具分析应用程序的性能,从而解决这些问题。 通过了解在哪里以及如何改进应用程序的性能,开发人员可以实现优化,使代码更快、更高效。 最终目标是提高应用程序的整体性能,使其运行更高效、更经济。

难题

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

  • 诊断性能瓶颈:主要挑战之一是准确地确定性能问题的根本原因。 CPU 使用率低加上性能缓慢可能有多种影响因素。 开发人员必须有效地使用分析工具来诊断这些问题,这需要对这些工具的工作原理以及如何解释其输出有一定的了解。

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

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

策略

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

  • 我们通过在收集性能数据的同时观察 .NET 计数器指标来开始调查。 与 CPU 使用率工具一样,Visual Studio 的 .NET 计数器工具也是进行性能调查的良好起点。
  • 接下来,为了获得更多见解以帮助隔离问题或提高性能,请考虑使用其他分析工具之一收集跟踪。 例如,使用检测工具查看调用计数和计时数据。

数据收集需要执行以下任务:

  • 将应用设置为发行版本。
  • 从性能探查器 (Alt+F2) 中选择 .NET 计数器工具。 (后续步骤会涉及检测工具。)
  • 在性能探查器中,启动应用并收集跟踪。

检查性能计数器

在运行应用程序时,我们会观察 .NET 计数器工具中的计数器。 初步调查时要关注的一些关键指标包括:

  • CPU Usage。 观察此计数器,确定出现性能问题时的 CPU 使用率是高还是低。 这可能是特定类型性能问题的线索。 例如:
    • 在 CPU 使用率较高的情况下,使用 CPU 使用率工具来确定我们可以优化代码的领域。 有关此内容的教程,请参阅案例研究:初学者代码优化指南
    • 如果 CPU 使用率低,可使用检测工具,根据壁挂时钟时间识别调用计数和平均函数时间。 这可能有助于识别争用或线程池不足等问题。
  • Allocation Rate。 对于服务请求的 Web 应用,此速率应该相当稳定。
  • GC Heap Size。 观察此计数器,确定内存使用率是否在持续增长并可能存在泄漏。 如果它看起来很高,请使用其中一个内存使用工具。
  • Threadpool Thread Count。 对于服务请求的 Web 应用,观察此计数器,确定线程计数是保持稳定还是以稳定速率上升。

下面的示例显示 CPU Usage 水平低,而 ThreadPool Thread Count 相对较高。

.NET 计数器工具中显示的计数器屏幕截图。

如果 CPU 使用率低,但线程计数稳定上升,可能说明线程池饥饿。 线程池被迫持续启动新线程。 当池中没有可用于处理新工作项的线程时,则会发生线程池饥饿现象,这通常会导致应用程序响应缓慢。

如果 CPU 使用率低但线程计数相对较高,并根据可能存在线程池饥饿问题的原理,这时可切换到使用检测工具。

调查调用计数和计时数据

让我们看一下检测工具中的跟踪,看看是否能够更深入了解线程发生了什么。

使用检测工具收集跟踪并将其加载到 Visual Studio 后,我们首先检查显示汇总数据的初始 .diagsession 报告页。 在收集的跟踪中,我们使用报告中的打开详细信息链接,然后选择火焰图

检测工具中的火焰图屏幕截图。

火焰图可视化显示,QueryCustomerDB 函数(以黄色显示)占应用程序运行时间的很大一部分。

右键单击 QueryCustomerDB 函数,然后选择“在调用树中查看”

检测工具中的调用树屏幕截图。

应用中 CPU 使用率最高的代码路径称为热路径。 热路径火焰图标 (显示“热路径”图标的屏幕截图。) 有助于快速识别可能改进的性能问题。

调用树视图中,你可以看到热路径包含 QueryCustomerDB 函数,该函数指向潜在的性能问题。

相比在其他函数中花费的时间,QueryCustomerDB 函数的 Self 和 Avg Self 值非常高。 与 Total 和 Avg Total 不同,Self 值排除了在其他函数中花费的时间,因此这是调查性能瓶颈的好位置。

提示

如果 Self 值并不高,相对较低,则可能需要调查 QueryCustomerDB 函数调用的实际查询。

双击 QueryCustomerDB 函数以显示该函数的源代码。

public ActionResult<string> QueryCustomerDB()
{
    Customer c = QueryCustomerFromDbAsync("Dana").Result;
    return "success:taskwait";
}

我们做了一些研究。 或者,我们可以节省时间,让 Copilot 为我们做研究

如果我们使用 Copilot,我们从上下文菜单中选择询问 Copilot,然后键入以下问题:

Can you identify a performance issue in the QueryCustomerDB method?

提示

可以使用斜杠命令(如 /optimize)来帮助 Copilot 形成好的问题。

Copilot 告诉我们,这段代码调用了一个异步 API,而没有使用 await。 这属于 sync-over-async 代码模式,是导致线程池饥饿的常见原因,并且可能会阻止线程。

要解决此问题,请使用 await。 在此示例中,Copilot 提供了以下代码建议以及说明。

public async Task<ActionResult<string>> QueryCustomerDB()
{
    Customer c = await QueryCustomerFromDbAsync("Dana");
    return "success:taskwait";
}

如果看到与数据库查询相关的性能问题,则可以使用数据库工具来调查是否有某些调用较慢。 此数据可能表明存在优化查询的机会。 有关演示如何使用数据库工具调查性能问题的教程,请参阅案例研究:初学者代码优化指南。 数据库工具支持将 .NET Core 与 ADO.NET 或 Entity Framework Core 配合使用。

要在 Visual Studio 中获取单个线程行为的可视化效果,可以在调试时使用并行堆栈窗口。 此窗口会显示单个线程以及有关处于等待状态的线程、这些线程正在等待完成的线程以及死锁的信息。

有关线程池饥饿的详细信息,请参阅检测线程池饥饿

后续步骤

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