java的jit编译器如何使坏代码比好代码快?

o2g1uqev  于 2021-06-30  发布在  Java
关注(0)|答案(1)|浏览(517)

背景

这是一个从数据结构和算法讲座开始的研究。很抱歉我的背景很长,但有必要理解这个问题。
对lomuto和hoare的划分算法进行了基准测试,以确定它们的运行时间是否与理论预测的时间相符。分析表明,根据输入数组n的长度,它们执行的交换次数约为:
洛木托: n/2 霍尔: n/4 ; 如果大小数在数组中均匀分布
霍尔: n/6 ; 如果大小数字在数组中分布不均匀
交换是这些算法中最昂贵的操作,因此可以很好地估计它们在运行时间方面的比较。这意味着,如果数组是随机的,大小数分布相等,hoare的运行时间为 20 micros ,洛木托大约需要 40 micros ,速度慢2倍。
微基准:

public static double averageRuntimeTests(Function<int[], Integer> algorithm, int size,
                                         int warmupIterations, int measureIterations, int repeatedExecs) {
    long start, end;
    int result = -1;
    double[] runningTimes = new double[measureIterations];

    // Warmup
    for (int i = 0; i < warmupIterations; i++) {
        int[] A = randomArray(size);
        for (int j = 0; j < repeatedExecs; j++)
            result = algorithm.apply(A);
        System.out.print(result);
    }

    // Measure
    for (int i = 0; i < measureIterations; i++) {
        int[] A = randomArray(size);
        start = System.nanoTime();
        for (int j = 0; j < repeatedExecs; j++)
            result = algorithm.apply(A);
        end = System.nanoTime();
        System.out.print(result);
        runningTimes[i] = (end - start) / (repeatedExecs * 1000.0);
    }

    return average(runningTimes);
}

如果算法在同一个输入数组上运行足够多次,使得jit编译器“有足够的时间”来优化代码,那么微基准与理论是一致的。请参阅下面的代码,其中算法针对每个不同的数组运行30次。

>>>案例1

public static void main(String[] args) {
    int size = 10_000, warmupIterations = 10_000, measureIterations = 2_000, repeatedExecs = 30;

    System.out.printf("input size = %d%n", size);
    System.out.printf("#warmup iterations = %d%n", warmupIterations);
    System.out.printf("#measure iterations = %d%n", measureIterations);
    System.out.printf("#repeated executions = %d%n", repeatedExecs);

    double timeHoare = averageRuntimeTests(Partitioning::partitionHoare, size, warmupIterations,
            measureIterations, repeatedExecs);
    double timeLomuto = averageRuntimeTests(Partitioning::partitionLomuto, size, warmupIterations,
            measureIterations, repeatedExecs);

    System.out.printf("%nHoare: %f us/op%n", timeHoare);
    System.out.printf("Lomuto: %f us/op%n", timeLomuto);
}

结果:
洛木托: 7.94 us/op 霍尔: 3.7685 us/op 对于均匀分布的阵列,lomuto大约比hoare慢2倍。
lomuto在hoare之前运行时的结果:
洛木托: 13.513 us/op 霍尔: 3.5865 us/op 洛穆托比霍尔慢4倍,比它应该慢得多。由于某些原因,如果它在霍尔之前运行,所需时间几乎是原来的两倍。

问题

但是,如果算法对每个不同的输入数组只运行一次,jit编译器的行为就会出人意料。

>>>案例2

public static void main(String[] args) {
    int size = 10_000, warmupIterations = 300_000, measureIterations = 60_000, repeatedExecs = 1;

    System.out.printf("input size = %d%n", size);
    System.out.printf("#warmup iterations = %d%n", warmupIterations);
    System.out.printf("#measure iterations = %d%n", measureIterations);
    System.out.printf("#repeated executions = %d%n", repeatedExecs);

    double timeHoare = averageRuntimeTests(Partitioning::partitionHoare, size, warmupIterations,
            measureIterations, repeatedExecs);
    double timeLomuto = averageRuntimeTests(Partitioning::partitionLomuto, size, warmupIterations,
            measureIterations, repeatedExecs);

    System.out.printf("%nHoare: %f us/op%n", timeHoare);
    System.out.printf("Lomuto: %f us/op%n", timeLomuto);
}

结果(无论hoare是否在lomuto之前运行):
洛木托: 26.676133 us/op 霍尔: 31.8233 us/op 令人震惊的是,看到洛穆托甚至比霍尔快!jit编译器在这里做什么?
我一直在说jit编译器应该受到指责,因为如果我完全禁用它并在解释器模式下运行(使用 -Djava.compiler=NONE 标志)基准再次如预期的那样。为每个不同的数组运行一次算法。。。

>>>案例3

结果(无论hoare是否在lomuto之前运行):
洛木托: 597.76 us/op 霍尔: 254.0455 us/op 正如你所看到的,洛穆托再次慢了大约2倍。
有人能解释一下案例2中的jit编译器是怎么回事吗?看起来jit编译器只是部分优化了代码。但是,为什么洛穆托和霍尔一样快呢?你不应该再快点吗?

请注意:

我知道有jmh库可以在java中可靠地运行微基准。我只是想理解jit编译器的底层机制。
微秒是微秒的缩写,和我们一样。

kzipqqlq

kzipqqlq1#

java的jit编译器如何比好代码运行得更快?
jit编译器不运行代码。它编译代码。
你所看到的不是(有效的)坏代码比好代码运行快的证据。
我知道有jmh库可以在java中可靠地运行微基准。我只是想理解jit编译器的底层机制。
jit编译器做什么可能并不重要。可能是当它这样做的时候,才引起了问题。
jit编译器使用cpu时间进行优化。如果以简单的方式实现一个微基准测试,那么jit编译器的部分或全部时间将包含在基准测试的一个(或多个)迭代中。这将使基准迭代(一个或多个迭代)看起来比较早或稍后的迭代花费更长的时间。这扭曲了明显的执行时间。
仔细想想,java基准测试的运行速度有三种或三种以上:
它开始运行得很慢,因为jvm正在解释字节码并收集统计数据。
一旦收集了足够的统计数据,jvm(显然)就会在jit编译器编译和优化字节码时运行得非常慢。
一旦jit编译器完成了它的工作,jvm就开始执行编译的本机代码,代码运行得更快。
如果你使用 jmh (或类似的)它应该补偿jit编译和其他异常的影响。如果您想了解更多信息,请阅读如何用java编写正确的微基准测试。。。这有助于解释常见的陷阱。
1-我指的是使用系统时钟测量前后获得的视在速度。

相关问题