本文发表于 1627 天前,其中的信息可能已经事过境迁
文章摘要

前几天,有位朋友问我,你平时都是怎么去排查一个程序的性能问题的啊。不要误会,这位朋友不是我啦,因为我真的有这样一位叫做 Toby 的朋友。说到性能问题,可能大家立马会想到类似并发数吞吐量响应时间QPSTPS等等这些指标,这些指标的确可以反映出一个系统性能的好坏。可随着我们的系统结构变得越来越复杂,要找到这样一个性能的“损耗点”,同样会变得越来越困难。在不同的人的眼中,对于性能好坏的评判标准是不一样的,譬如在前端眼中,页面打开速度的快慢代表着性能的好坏;而在后端眼中,并发数、吞吐量和响应时间代表着性能的好坏;而在 DBA 眼中,一条 SQL 语句的执行效率代表着性能的好坏。更不用说,现实世界中的程序要在硬件、网络的世界里来回穿梭了,所以,从 80%的功能堆积到 100%,是件非常容易的事情;而从 80%的性能优化到 85%,则不是件非常轻松的事情。想清楚这一点非常简单,因为我们的系统从来都不是简单的1 + 1 = 2。此时,我们需要一个性能分析工具,而今天给大家分享的是 JetBrains 出品的 dotTrace

快速开始(Quick Start)

安装软件的过程此处不表,这里建议大家同时安装 dotTracedotMemery。因为这都是 JetBrains 全家桶中的软件,安装的时候选一下就可以了,可谓是举手之劳。安装好以后的界面是这样的,可以注意到,它可以对进程中的 .NET 应用、本机的 .NET 应用以及远程的 .NET 应用进行检测,因为这里写一个 .NET Core 应用来作为演示,所以,我们选择 Profile Local App

dotTrace主界面 dotTrace主界面

在这里,我们准备了一个简单的控制台程序:

public class Program
{
    static void Main(string[] args)
    {
        CPUHack();
        MemeryHack();
    }
        
    public static void MemeryHack() {
        Console.ReadLine();
        var bytes = GC.GetTotalAllocatedBytes();
        Console.WriteLine($"AllocatedBytes: { bytes } bytes");
        var list = new List<byte[]>();
        try
        {
            while (true) {
              list.Add(new byte[85000]);
            }
        } catch (OutOfMemoryException) {
            Console.WriteLine(nameof(OutOfMemoryException));
            Console.WriteLine(list.Count);
            bytes = GC.GetTotalAllocatedBytes();
            Console.WriteLine($"AllocatedBytes: { bytes } bytes");
        }

        Console.ReadLine();
    }

    public static void CPUHack() {
        Parallel.For(0, Environment.ProcessorCount,
            new ParallelOptions() {
                MaxDegreeOfParallelism = Environment.ProcessorCount
            },
            i => {
              
        });
    }
}

其中,CPUHack()方法来自:打爆你的 CPU; MemeryHack()方法来自:通过代码实现 OutOfMemory。顾名思义,我们将利用这两个方法来分别测试 dotTracedotMemery

dotTrace 目前支持以下平台:.NET、.NET Core、WPF、UWP(Universal Windows Platform)、ASP.NET、Windows 服务、WCF、Mono 和 Unity。可以注意到它有四种监测方式,即 Sampling、Tracing、Line by Line 以及 Timeline。按照界面上的描述,Sampling 适用于大多数场景下调用时间的精确测量、Tracing 适用于算法复杂度分析场景下调用次数的精确测量、Line by Line 适用于更高级别的使用场景,Timeline 适用于含多线程在内的数据处理的精确测量。所以,我们这里选择好一个可执行文件,然后选择 Sampling,再点击 “Run”:

对进行程序进行采样、生成快照 对进行程序进行采样、生成快照

此时,我们会看到对应程序的的工具栏,我们可以点击 “Get Snapshot and Wait” 进行采样,每次采样会生成一个快照,默认情况下会自动打开生成的快照。我们还可以点击 “Start” 重新进行采样,直至采集到满意的样本为止,而在完成采样后,则可以点击 “Kill” 结束采样。下面来看看生成的快照:

dotTrace性能快照 dotTrace性能快照

通过这两图,我们可以非常清晰的看到,最耗时的正是我们这里的CPUHack()方法,并且这里一共有四个线程,这是因为博主的计算机使用的是一款 4 核的 i3 处理器,并且在dotTrace中可以直接看到相关的代码片段,当然,这一切的前提是你没有对应用程序做过混淆处理,这样,我们就完成了一个简单的性能分析。类似地,我们启动dotMemery。此时,可以得到下面的结果:

dotMemery内存分析 dotMemery内存分析

这里,我们通过<YourApp>.runtimeconfig.json文件,设定了 GC 堆的最大值是 1M,而每次向列表中添加超过 85K 的 byte 数组时,当前对象会被分配到大对象堆上。通过这张图我们可以很清楚的看到,整个曲线中蓝色区域的 LOH 占了绝对的比例,换言之,几乎所有的内存都是分配到大对象堆(LOH)上的。此外,有些小对象从 0 代升到了 1 代,在这个例子中,由于可分配的内存不足,最终引发了OutOfMemoryException。而这和我们看到的结果是相符合的:

{
  "runtimeOptions": {
    "tfm": "netcoreapp3.1",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "3.1.0"
    },
    "configProperties": {

      "System.GC.HeapHardLimit": 1048576

    }
  }
}

从 Dump 文件进行分析

到此为止,关于 dotTracedotMemery 的使用就基本上讲解完啦!可能这时候有些朋友会产生疑问,如果性能问题发生在生产环境怎么办啊。不错,这里我们调试的都是本地的程序,生产环境是没有机会让你这样去搞的。此时,我们可以借助内存转储文件(Dump)文件,它是进程的内存镜像,可以把程序的执行状态通过调试器保存在 Dump 文件中,试想一下,如果程序在前一秒崩溃了,而你在这一瞬间获得了当时程序的状态信息,相当于拿到了“故障”遗留在现场的“罪证”。在 Windows 系统中创建 Dump 文件是非常简单的,通过任务管理器->创建转储文件即可完成,我们继续使用上面提到的例子:

创建Dump文件 创建Dump文件

其实,拿到 Dump 文件以后,分析它的工具非常多,比如常见的 WinDBG、DebugDiag 等等,这里我们可以直接使用 dotMemery ,因为它本身就支持 Dump 文件的导入,相比前面两种在使用上要更加友好一点。此时,导入这个 Dump 文件,我们就可以获得下面的结果:

大对象堆分布情况 大对象堆分布情况

一、二代GC分布情况 一、二代GC分布情况

这和我们前面分析出的结论是一致的,即,几乎所有的内存都是分配到大对象堆(LOH)上的。除此以外,针对.NET Core,官方提供了 dotnet-dumpdotnet-gcdump两个命令行工具,可以通过下面的命令安装:

dotnet tool install -g dotnet-dump
dotnet tool install -g dotnet-gcdump

这两个命令同样可以对内存进行分析,关于更多的.NET Core 的诊断教程,请参考:https://docs.microsoft.com/zh-cn/dotnet/core/diagnostics/event-counter-perf,这些细节都是针对.NET Core 的,可能不具有普适性,感兴趣的朋友可以自行前去了解。和大多数JetBrains的应用一样,这些程序都有 Visual Studio 的扩展程序,可以直接集成到 Visual Studio 中,这个同样看个人喜好,不再详细讲解。

本文小结

结合一个简单的示例程序,本文简单地介绍了来自 JetBrains 的两款软件 dotTracedotMemery 的基本使用,以及如何通过内存转储文件(Dump)对生产环境中的内存进行诊断。在以往的关于程序性能优化的经历中,我个人还使用过 ANTS-Performance-Profiler 这个软件,但体验上感觉还是 dotTracedotMemery 稍微好用一点,而对于更一般的代码角度的性能分析,我推荐一个轻量级的项目MiniProfiler,性能优化不能靠猜,可是从初中就开始学的“控制变量法”未尝不是一个不错的思路。刷 LeetCode 的这段时间,一个最大的感悟就是,程序的性能,真的是一点一点的优化出来的,就拿最简单的排序来说,你真的要在上面提交很多次,才能渐渐地明白为什么说有些排序算法是“不稳定”的。也许,现在硬件水平越来越好,我们不必像前辈们一样“锱铢必较”,可这一切其实很都公平,你写代码的时候有多浪费,你玩游戏的时候就有多心疼,这里要特别表扬育碧对叛变这一作的优化。好了,这就是这篇博客的内容啦,谢谢大家,晚安!

参考链接

赞赏博主
相关推荐 随便逛逛
Semantic Kernel × MCP:智能体的上下文增强探索 本文深入探讨了 MCP(模型上下文协议),由 Anthropic 设计的开放协议,它如同 AI 领域的 USB 接口,旨在通过统一接口解决大模型连接不同数据源和工具的问题。文章详细介绍了 MCP 的架构、核心角色、工作原理以及如何与 Semantic Kernel 集成,为 .NET 开发者提供高效接入社区 MCP 服务器的方法,减少重复性平台对接工作。此外,还展示了 MCP 在操作浏览器、访问文件系统等场景中的应用效果,并探讨了其局限性及未来发展方向。在 AI 技术快速发展的背景下,MCP 的出现为实现 AI 模型的 “万物互联” 提供了可能,值得开发者关注与探索。
基于 K-Means 聚类分析实现人脸照片的快速分类 本文介绍了使用 K-Means 聚类算法对人脸照片进行自动分类的方法,解决了 “脸盲症” 问题。通过 Dlib 提取人脸特征向量,并利用 Scikit-Learn 的 K-Means 聚类分析,能够快速将大量人脸照片按人物进行分类。文章详细讲解了 K-Means 算法的原理、K 值确定方法,以及如何使用 PCA 降维和 Matplotlib 可视化聚类结果。该技术方案帮助用户高效地整理和管理照片,避免了人工分类的繁琐过程。尽管 K-Means 算法简单高效,但其对簇数和初始中心的选择较为敏感,可能不适用于噪声数据和非凸形簇,建议在实际应用中结合 DBSCAN 等算法以提高聚类效果。
评论 隐私政策