评测Java代码会更改执行时间



我试图优化我的代码,但它给我带来了问题。我有一个对象列表:

List<DataDescriptor> descriptors;
public class DataDescriptor {
public int id;
public String name;
}

有1700个对象具有唯一id(0-1699)和一些名称,它用于解码我稍后得到的数据类型

我试图优化的方法是这样的:

public void processData(ArrayList<DataDescriptor> descriptors, ArrayList<IncomingData> incomingDataList) {
for (IncomingData data : incomingDataList) {
DataDescriptor desc = descriptors.get(data.getDataDescriptorId());
if (desc.getName().equals("datatype_1")) {
doOperationOne(data);
} else if (desc.getName().equals("datatype_2")) {
doOperationTwo(data);
} else if ....
.
.
} else if (desc.getName().equals("datatype_16")) {
doOperationSixteen(data);
}
}
}

当处理数据文件时,这个方法被调用了大约一百万次,每次incomingDataList包含大约60个元素,所以这组if/else被执行了大约6000万次。

这在我的桌面上大约需要15秒(i7-8700)。

更改代码以测试整数id而不是字符串显然可以节省几秒钟的时间,这很好,但我希望更多:)我尝试过使用VisualVM进行评测,但对于这种方法(使用字符串测试),它说66%的时间花在"自我时间"上(我相信这将是所有的字符串测试?为什么它不说它在string.equals方法中?),33%花在descriptors.get上——这是从ArrayList获得的简单方法,我认为我无法进一步优化它,除了试图改变数据在内存中的结构(不过,这是Java,所以我不知道这是否会有很大帮助)。

我编写了"简单基准测试"应用程序来隔离这种String与int的比较。正如我所料,当我简单地运行应用程序时,比较整数比String.equals快大约10倍,但当我在VisualVM中分析它时(我想检查在基准ArrayList.get中是否也会很慢),奇怪的是,两种方法花费的时间完全相同。当使用VisualVM的Sample而不是Profile时,应用程序完成了预期的结果(int快10倍),但VisualVM显示,在他的示例中,两种类型的比较花费的时间相同。

在分析和不分析时,得到如此完全不同的结果的原因是什么?我知道有很多因素,JIT和评测可能会干扰它等等——但最终,当评测工具改变代码的运行方式时,你如何评测和优化Java代码?(如果是这样的话)

Profiler可分为两类:检测和采样。VisualVM包括这两者,但两者都有缺点。

检测分析程序使用字节码检测来修改类。他们基本上将特殊的跟踪代码插入到每个方法的入口和出口中。这允许记录所有执行的方法及其运行时间。然而,这种方法有很大的开销:首先,因为跟踪代码本身可能需要很多时间(有时甚至比原始代码还要长);其次,因为插入指令的代码变得更加复杂,并且阻止了某些可以应用于原始代码的JIT优化。

采样配置文件不同。它们不会修改您的应用程序;相反,它们会定期获取应用程序正在执行的操作的快照,即当前运行线程的堆栈跟踪。某些方法在这些堆栈跟踪中出现的频率越高,该方法的总执行时间(从统计上)就越长。

采样轮廓器通常具有小得多的开销;此外,这种开销是可管理的,因为它直接取决于分析间隔,即探查器获取线程快照的频率。

采样分析器的问题在于JDK用于获取堆栈跟踪的公共API存在缺陷。JVM在任意时刻都不会得到堆栈跟踪。相反,它会将线程停在一个预定义的位置,在那里它知道如何可靠地遍历堆栈。这些地方被称为安全点。安全点位于方法出口(不包括内联方法)和循环内部(不包括短计数循环)。这就是为什么,如果您有一个长的线性代码和平或一个短的计数循环,您将永远不会在依赖JVM标准getStackTraceAPI的采样剖析器中看到它。

此问题被称为安全点偏差。Nitsan Wakart在一篇伟大的帖子中对其进行了很好的描述。VisualVM并不是唯一的受害者。许多其他评测器,包括商业工具,也会遇到同样的问题,因为最初的问题在JVM中,而不是在特定的评测工具中。

Java飞行记录器要好得多,只要它不依赖安全点。然而,它也有自己的缺陷:例如,当线程执行某些JVM内部方法(如System.arraycopy)时,它无法获得堆栈跟踪。这尤其令人失望,因为arraycopy是Java应用程序中经常出现的瓶颈。

尝试异步探查器。该项目的目标正是为了解决上述问题。它应该提供应用程序性能的公平视图,同时开销非常小。async探查器适用于Linux和macOS。如果你在Windows上,JFR仍然是你最好的选择。

最新更新