分析 Java 代码更改执行时间

Profiling Java code changes execution times

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

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/elses被执行了大约6000万次。

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

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

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

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

分析器可分为两类:检测和采样。 VisualVM 两者都包含,但两者都有缺点。

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

采样分析器不同。他们不会修改您的申请;相反,他们会定期拍摄应用程序正在执行的操作的快照,即当前 运行 线程的堆栈跟踪。某些方法在这些堆栈跟踪中出现的频率越高 - 此方法的总执行时间(统计上)越长。

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

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

这个问题被称为安全点偏差。 Nitsan Wakart 在 a great post 中对其进行了很好的描述。 VisualVM 并不是唯一的受害者。许多其他分析器,包括商业工具,也遇到同样的问题,因为最初的问题是在 JVM 而不是在特定的分析工具中。

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

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