教程:CPU 分析入门
最后修改时间:2023 年 8 月 24 日有时您的应用程序可以运行,但您希望通过提高吞吐量或减少延迟来提高性能。其他时候,您只是想知道代码在运行时的行为方式,确定热点在哪里,或者弄清楚框架在幕后如何运行。
部分地,您可以在设计时获取这些信息,因为 IntelliJ IDEA 为您提供了可以静态生成的各种线索。当然,仅通过查看代码无法了解所有错误和低效率,因为事情在运行时会变得更加复杂。如果手头没有合适的工具,验证您的任何猜测都可能是一项艰巨的任务。
分析器提供了任意大执行块的鸟瞰图。它不会干扰正在运行的程序,也不会像调试器那样提供粒度数据。然而,它可以收集其他工具无法收集的有价值的信息,这就是为什么在本教程中我们将了解 IntelliJ IDEA Profiler 的功能。
许多人认为,只要不编写高负载应用程序,他们就不需要学习如何分析。在示例中,我们将看到即使在处理非常简单的应用程序时,我们也可以如何从分析中受益。
示例应用程序
假设我们有以下应用程序:https ://github.com/flounder4130/profiler-example 。
它反复尝试在文件系统中创建路径(我们使用createDirectories()
NIO2 中的路径)。如果尝试失败,则不会引发异常。我们使用临时基准来测量吞吐量。
每次任务运行时,基准测试逻辑都会将当前时间戳存储到集合中,并删除指向早于当前时间减去某个间隔的时间的所有时间戳。这样就可以通过查询集合来找出在该时间间隔内发生了多少事件。这个工作流程应该可以帮助我们了解应用程序的执行情况。
当我们运行程序时,我们发现它的表现没有达到我们的预期:
Average count: 3527 op
Spent time: 3052 ms
让我们分析一下它,看看出了什么问题。
设置
IntelliJ Profiler 不需要任何特殊设置,但是,在本例中我们需要使用本机分析,默认情况下禁用。
在快照中包含本机示例
按打开 IDE 设置,然后选择“构建”、“执行”、“部署”| Java 探查器。选中收集本机呼叫复选框。CtrlAlt0S
使用分析器运行
单击应用程序入口点附近(
CountEvents
我们示例中的类)。从菜单中,选择使用 IntelliJ Profiler 分析 CountEvents.main()。
当应用程序运行完毕后,会出现一个绿色的弹出窗口,提示我们打开报告。如果我们错误地关闭了弹出窗口,该报告仍将在Profiler工具窗口中可用。
让我们打开报告,看看里面有什么内容。
分析个人资料
打开报告后我们首先看到的是火焰图。火焰图上的数据本质上是所有采样堆栈的汇总。分析器收集的具有相同堆栈的样本越多,该堆栈在火焰图上增长得越宽。因此,帧的宽度大致相当于处于该状态的时间份额。至于颜色:堆栈的黄色部分是 Java 代码,蓝色是本机方法调用。
令我们惊讶的是,该createDirectory()
方法并没有占用最多的执行时间。我们自制的基准测试花费了大约相同的时间来执行!此外,如果我们查看上面的一帧,我们会发现这主要是因为该removeIf()
方法,该方法几乎占据了其父方法 的所有时间update()
。
这显然需要一些调查。
public static int update(Deque<Long> events, long nanos, long interval) {
events.add(nanos);
events.removeIf(aTime -> aTime < nanos - interval);
return events.size();
}
显然,removeIf()
执行需要很长时间,因为它会迭代整个集合,尽管实际上并不需要这样做。
优化代码并验证结果
由于我们使用有序集合,并且事件按时间顺序添加,因此我们可以确保所有要删除的元素始终位于队列的头部。如果我们替换removeIf()
为一个循环,一旦它开始迭代不会删除的事件就会中断,我们可以潜在地提高性能:
while (events.peekFirst() < nanos - interval) {
events.removeFirst();
}
让我们再次分析我们的应用程序并查看结果。用于搜索火焰图:Ctrl0F
当我们搜索该update()
方法时,我们发现它已经成为图表上的一小部分,并且不再具有巨大的开销。createDirectories()
现在占据了更大份额的申请时间。
该应用程序还按数字返回不错的结果:
Average count: 9237 op
Spent time: 1090 ms
现在好多了,不是吗?尽管功能风格可能简洁明快,但我们应该始终考虑它是否真正满足我们的需要。有时,好的旧循环可以为我们节省大量宝贵的处理器时间。
进一步优化
我们现在可以停下来拍拍自己的背,但是我们的createDirectories()
方法出了什么问题?火焰图有说明吗Exception
?
如果我们检查调用的堆栈顶部部分createDirectories()
,我们会看到许多似乎处理异常的本机框架。但我们的应用程序没有崩溃,我们也没有处理任何问题,那么为什么会发生这种情况呢?
进一步的调查和放大会给我们答案。不需要太多的 Java 经验就可以读取堆栈上的方法名称并得出异常与尝试创建已存在的文件有关的结论。
让我们尝试避免这种情况并将调用包装在检查createDirectories()
中Files.exists()
:
Path p = Paths.get("./a/b");
if (!Files.exists(p)) {
Files.createDirectories(p);
}
哇!我们的代码现在快如闪电:
Average count: 48453 op
Spent time: 143 ms
现在速度比原来快了大约 21 倍。这个异常处理真的很昂贵!在您的计算机上,结果可能会有所不同,但无论如何它们都应该令人印象深刻。
概括
在本教程中,我们使用 IntelliJ IDEA Profiler 来检测和修复性能问题,并见证了即使是使用众所周知的 API 的简单代码也可能对应用程序的执行时间产生真正的影响。如果我们在现实生活中遇到这样的问题,我们可以使用相同的方法来诊断我们自己的代码和库代码。
该教程远非详尽无遗:IntelliJ IDEA 提供了很多分析分析器报告的方法,火焰图只是其中之一。分析器可能有用的情况范围也很大。如果您想了解更多方法并获得有关 IntelliJ IDEA 的更深入的知识,请查看本节中的其他主题。
感谢您的反馈意见!