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