首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >为什么JMH报告了如此奇怪的时间来进行简单的快速排序--显然与N* log(N)不相称?

为什么JMH报告了如此奇怪的时间来进行简单的快速排序--显然与N* log(N)不相称?
EN

Stack Overflow用户
提问于 2021-05-17 14:04:28
回答 1查看 320关注 0票数 2

为了研究一种排序算法(我自己的),我决定将它的性能与经典的quicksort进行比较。令我非常惊讶的是,我发现实现quicksort所花费的时间与N log(N)远远不成比例。我彻底地试图在我的quicksort中找到一个错误,但没有成功。这是一个简单的排序算法版本,它使用不同大小的Integer数组,填充了随机数,我不知道在哪里会出现错误。我甚至计算了我的代码执行的所有比较和交换,它们的数量与N log(N)相当成比例。我完全糊涂了,无法理解我所观察到的现实。以下是对1,000、2,000、4,000、8,000和16,000个随机值进行排序的基准结果(用JMH度量):

代码语言:javascript
运行
复制
Benchmark                       Mode  Cnt       Score     Error  Units    2N / N ratio          
QSortBenchmarks.sortArray01000  avgt    5     561.505 ±   2.992  us/op              
QSortBenchmarks.sortArray02000  avgt    5    2433.307 ±  11.770  us/op    4.334 
QSortBenchmarks.sortArray04000  avgt    5    8510.448 ±  34.051  us/op    3.497 
QSortBenchmarks.sortArray08000  avgt    5   38269.492 ± 161.010  us/op    4.497 
QSortBenchmarks.sortArray16000  avgt    5  147132.524 ± 261.963  us/op    3.845 

显然,我观察到的时间复杂度离O(n log(n))很远,几乎是O(n^2)。可能会有一种极小的怀疑,即随机种子已经降临到如此不幸的情况下,数组中的值恰好接近最坏的情况。这个概率非常接近于0,但不是0。但一些不同的随机种子的结果与此非常相似。

下面是比较和交换的数量(对于每个大小的随机填充数组的40个迭代):

代码语言:javascript
运行
复制
                     Avr. Comparisons      Avr. Swaps                     2N / N ratio
sortArray01000():           12119.925,       2398.925             
sortArray02000():           26581.600,       5268.525                     2.193, 2.196
sortArray04000():           59866.925,      11451.625                     2.252, 2.174
sortArray08000():          127731.025,      25006.425                     2.134, 2.184
sortArray16000():          273409.925,      54481.525                     2.141, 2.179

每个人都可以看到,操作的数量相当符合O(Nlog(N))定律。

甚至有可能怀疑单个操作的成本取决于我们处理的数组的大小,我已经检查了它是否是真的(使用一个简单的方法来反转不同大小的数组) --而不是,正如预期的那样,情况并非如此。时间非常接近O(n)

我唯一能想到的是,我的代码中有一些复杂的逻辑错误,但我无法理解。

有人能帮我吗?

下面是代码:

代码语言:javascript
运行
复制
import java.io.IOException;
import java.util.Locale;
import java.util.Random;
import java.util.function.Consumer;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/**
 * Why does quicksort take time disproportionate to N * log(N)?
 * Rectified for StackOverflow
 * 21.05.17 16:20:01
 */

@State(value = Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(java.util.concurrent.TimeUnit.MICROSECONDS)
@Fork(value = 2)
@Warmup(iterations = 5, time = 10)
@Measurement(iterations = 5, time = 10)
public class QSortBenchmarks {

  private static final int LOOPS_TO_ITERATE = 40; // 40;

  private static final int RAND_SEED = 123456789;
  private static final int RAND_LIMIT = 10000;
  private static final Random RAND = new Random(RAND_SEED); // Constant seed for reproducibility;

  private static int cmpCount = 0, swapCount = 0;
  private static int cmpTotal = 0, swapTotal = 0;

  private static final Integer[] array01000 = new Integer[1000];
  private static final Integer[] array02000 = new Integer[2000];
  private static final Integer[] array04000 = new Integer[4000];
  private static final Integer[] array08000 = new Integer[8000];
  private static final Integer[] array16000 = new Integer[16000];

  @Setup
  public static void initData() {
    cmpCount = 0; swapCount = 0;

    fillWithRandoms(array01000);
    fillWithRandoms(array02000);
    fillWithRandoms(array04000);
    fillWithRandoms(array08000);
    fillWithRandoms(array16000);
  }

  public static void main(String[] args) throws IOException, RunnerException {
    Locale.setDefault(Locale.US);
    initData();

    runJMH(); // Run benchmarks. Comment-out it, if you want just to count comparisons etc.
//    System.exit(0); // If don't want to count comparisons and swaps

    System.out.printf("\nRand seed = %d, rand limit = %d, iterations = %d\n",
                      RAND_SEED, RAND_LIMIT, LOOPS_TO_ITERATE);

    System.out.print("sortArray01000(): ");
    loopOverMethod(qq -> sortArray01000());

    System.out.print("sortArray02000(): ");
    loopOverMethod(qq -> sortArray02000());

    System.out.print("sortArray04000(): ");
    loopOverMethod(qq -> sortArray04000());

    System.out.print("sortArray08000(): ");
    loopOverMethod(qq -> sortArray08000());

    System.out.print("sortArray16000(): ");
    loopOverMethod(qq -> sortArray16000());
  }

  private static void loopOverMethod(Consumer<Object> method) {
    cmpTotal = 0; swapTotal = 0;
    for (int loops = 0; loops < LOOPS_TO_ITERATE; loops++ ) {
      initData();
      method.accept(null);;
      cmpTotal += cmpCount; swapTotal += swapCount;
    }
    System.out.printf("avrg compares: %12.3f,  swaps: %12.3f\n",
                      (double)cmpTotal / LOOPS_TO_ITERATE, (double)swapTotal / LOOPS_TO_ITERATE);
  }

  /**
   * @throws RunnerException
   */
  private static void runJMH() throws RunnerException {
    final Options opt = new OptionsBuilder()
        .include(QSortBenchmarks.class.getSimpleName())
        .forks(1)
        .build();
    new Runner(opt).run();
  }

  private static void fillWithRandoms(Integer[] array) {
    for (int i = 0; i < array.length; i++) { // // Fill it with LIST_SIZE random values
      array[i] = (int)(RAND.nextDouble() * RAND_LIMIT);
    }
  }

  @Benchmark
  public static void sortArray01000() {
    final Integer[] array = array01000;
    quickSort(array, 0, array.length - 1);
  }

  @Benchmark
  public static void sortArray02000() {
    final Integer[] array = array02000;
    quickSort(array, 0, array.length - 1);
  }

  @Benchmark
  public static void sortArray04000() {
    final Integer[] array = array04000;
    quickSort(array, 0, array.length - 1);
  }

  @Benchmark
  public static void sortArray08000() {
    final Integer[] array = array08000;
    quickSort(array, 0, array.length - 1);
  }

  @Benchmark
  public static void sortArray16000() {
    final Integer[] array = array16000;
    quickSort(array, 0, array.length - 1);
  }

  private static void quickSort(Integer[] array, int lo, int hi) {
    if (hi <= lo) return;
    final int j = partition(array, lo, hi);
    quickSort(array, lo, j - 1);
    quickSort(array, j + 1, hi);
  }

  private static int partition(Integer[] array, int lo, int hi) {
    int i = lo, j = hi + 1;
    while (true) {
      while (compare(array[++i], array[lo]) < 0) // while (array[++i] < array[lo])
        if (i == hi) break;
      while (compare(array[lo], array[--j]) < 0) // while (array[lo] < array[--j])
        if (j == lo) break;
      if (i >= j) break;
      swapItems(array, i, j);
    }
    swapItems(array, lo, j);
    return j;
  }

  private static int compare(Integer v1, Integer v2) {
    cmpCount++;
    return v1.compareTo(v2);
  }

  private static void swapItems(Integer[] array, int i, int j) {
    swapCount++;
    final Integer tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
  }

}

更新21-05-18 10:08 Z

形势已经好转了。正是JMH显示了如此奇怪的结果,与实际的执行时间没有任何共同之处。使用System.nanoTime()对10,000、20,000、40,000、80,000和160,000项数组进行简单的手工基准测试,显示了以下时间:

代码语言:javascript
运行
复制
Rand seed = 123, rand limit = 1000000, iterations = 1000
sortArray_010k(): avrg time:    1049.538 mks/op
sortArray_020k(): avrg time:    2282.189 mks/op
sortArray_040k(): avrg time:    4976.079 mks/op
sortArray_080k(): avrg time:   10762.461 mks/op
sortArray_160k(): avrg time:   23115.345 mks/op

虽然相同数组的JMH结果如下所示:

代码语言:javascript
运行
复制
Benchmark                       Mode  Cnt      Score     Error  Units
QSortBenchmarks.sortArray_010k  avgt    5    109.454 ±   0.444  ms/op
QSortBenchmarks.sortArray_020k  avgt    5    373.518 ±  17.439  ms/op
QSortBenchmarks.sortArray_040k  avgt    5   1350.420 ±  26.733  ms/op
QSortBenchmarks.sortArray_080k  avgt    5   6519.015 ±  48.770  ms/op
QSortBenchmarks.sortArray_160k  avgt    5  26837.697 ± 926.132  ms/op

手工基准测试显示的数字很好地符合N log(N),看起来很逼真,并且与观察到的执行时间在几秒钟内完全一致。它们比JMH显示的数字少100到1000多倍。

下面是获得它们的修改后的loopOverMethod()

代码语言:javascript
运行
复制
  private static void loopOverMethod(Consumer<Object> method) {
    for (int loops = 0; loops < 100; loops++ ) { // Kinda warmup
      initData();
      method.accept(null);
    }

    long time = 0;
    cmpTotal = 0; swapTotal = 0;
    for (int loops = 0; loops < LOOPS_TO_ITERATE; loops++ ) {
      initData();
      time -= System.nanoTime();
      method.accept(null);
      time += System.nanoTime();
      cmpTotal += cmpCount; swapTotal += swapCount;
    }
    System.out.printf("avrg time: \t%10.3f mks\n", 
                      time * 1e-3 / LOOPS_TO_ITERATE);
  }

但现在出现了另一个问题。为什么JMH会显示出如此奇怪的结果?我使用它的方式有什么不对?显然,我必须修改使用JMH的其他项目。一个令人不快的消息。

更新21-05-19 10:08 Z

把它放在这里作为问题的更新,因为评论不允许inser引号。托马斯·克莱格的全面回答是绝对正确的。在听取了他的建议后,我得到了以下结果:

JMH:

代码语言:javascript
运行
复制
Benchmark                       Mode  Cnt      Score    Error  Units
QSortBenchmarks.sortArray_010k  avgt    5    975.028 ± 23.907  us/op
QSortBenchmarks.sortArray_020k  avgt    5   2253.627 ± 94.108  us/op
QSortBenchmarks.sortArray_040k  avgt    5   4836.680 ± 80.964  us/op
QSortBenchmarks.sortArray_080k  avgt    5  10041.063 ± 27.796  us/op
QSortBenchmarks.sortArray_160k  avgt    5  21232.223 ± 32.008  us/op

手工基准:

代码语言:javascript
运行
复制
Rand seed = 123, rand limit = 1000000, iterations = 1000
sortArray_010k(): avrg time:    1060.951 mks
sortArray_020k(): avrg time:    2296.533 mks
sortArray_040k(): avrg time:    5021.629 mks
sortArray_080k(): avrg time:   10855.963 mks
sortArray_160k(): avrg time:   23335.923 mks

现在的结果看起来相当可信,我对它们完全满意。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2021-05-17 16:40:03

有三点是共同作用的,不利于你们的执行:

最糟糕的情况复杂性是O(n^2) (https://en.wikipedia.org/wiki/Quicksort#Choice_of_pivot):

  • ,选择最左边的元素作为枢轴,在已经排序的数组上给出最坏的情况行为

在较早版本的

中,分区的最左边元素通常被选择为pivot元素。不幸的是,这会导致已经排序的数组出现最坏的情况。

  • 您的算法对数组进行排序,这意味着在第一次传递之后,“随机”数组被排序。(计算JMH进行几次数据传递的平均次数)。

要解决这个问题,您可以更改基准测试方法。例如,您可以将sortArray01000()更改为

代码语言:javascript
运行
复制
@Benchmark
public static void sortArray01000() {
  final Integer[] array = Arrays.copyOf(array01000, array01000.length);
  quickSort(array, 0, array.length - 1);
}

或者您可以修改@Setup注释,以便在每次调用基准测试方法之前执行它:

代码语言:javascript
运行
复制
@Setup(Level.Invocation)
public static void initData() {
    //...
}

@Setup注释接受一个参数,该参数确定何时执行安装方法。

这三个层次是(https://hg.openjdk.java.net/code-tools/jmh/file/2be2df7dbaf8/jmh-core/src/main/java/org/openjdk/jmh/annotations/Level.java):

  • Level.Trial:在每个benchmark
  • Level.Iteration:之前,在每个iteration
  • Level.Invocation:之前,在每次执行基准测试方法

之前。

默认级别是Level.Trial (https://hg.openjdk.java.net/code-tools/jmh/file/2be2df7dbaf8/jmh-core/src/main/java/org/openjdk/jmh/annotations/Setup.java#l54)。

这对你的考试意味着什么?

要理解这一点,您必须了解JMH如何执行您的基准测试:

它为您的基准测试methods

  • during之一启动了一次测试。它每次迭代进行5次热身迭代和5次度量iterations

  • during,它在一个很紧的循环中调用您的基准测试方法,直到10秒过去,如果您的基准测试方法需要500 up,这意味着在每次迭代中它将被调用大约20'000次,或者在全面试用

期间调用大约200'000次。

现在,有了@Setup(Level.Trial)和一个对输入数据进行排序的基准方法,这意味着只有快速排序方法的第一个调用才能显示O(N log(N))行为,所有剩下的调用都在已经排序的数组上运行,并显示O(N^2)的最坏的情况行为。

对于@Setup(Level.Iteration),情况还好不了多少--现在它是每个迭代中具有O(N log(N))行为的基准方法的第一个调用,其余的每次迭代中大约有2万次调用仍然显示O(N^2)

对于@Setup(Level.Invocation),基准方法的每一次调用(因此每次调用快速排序)都会获得自己的未排序数组作为输入,这在结果中可以清楚地显示出来:

代码语言:javascript
运行
复制
@Setup(Level.Trial)

1000: 780 us
2000: 3300 us


@Setup(Level.Iteration)

1000: 780 us
2000: 3280 us
4000: 11700 us


@Setup(Level.Invocation)

1000: 58 us
2000: 124 us
4000: 280 us

通过我提议的更改(在基准测试方法中复制输入数组),我得到了稍微好一些的结果,但这可能是由于缓存的效果:

代码语言:javascript
运行
复制
1000: 25 us
2000: 108 us
4000: 260 us
票数 5
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/67571268

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档