Java 为什么排序较小的子数组首先会导致更快的快速排序?

Java 为什么排序较小的子数组首先会导致更快的快速排序?,java,optimization,quicksort,tail-recursion,Java,Optimization,Quicksort,Tail Recursion,我一直在尝试快速排序,并学习如何测试程序的速度。有一件事对我来说没有意义。我尝试在Java中实现一个使用尾部递归的快速排序算法。它通常跑得更快。但后来我了解到Java不支持尾部调用优化。为什么它会跑得更快呢 这是代码 import java.util.Arrays; import java.util.Random; import java.util.Scanner; import java.util.logging.Level; import java.util.logging.Logger;

我一直在尝试快速排序,并学习如何测试程序的速度。有一件事对我来说没有意义。我尝试在Java中实现一个使用尾部递归的快速排序算法。它通常跑得更快。但后来我了解到Java不支持尾部调用优化。为什么它会跑得更快呢

这是代码

import java.util.Arrays;
import java.util.Random;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

public class QuicksortCompare
{
    private final static Random rand = new Random();
    private static final int MIN_LENGTH = 10;
    private static final int MAX_LENGTH = 1000;
    private static final int MIN_VAL = -1000;
    private static final int MAX_VAL = 1000;

    public static void main(String[] args)
    {
        /*long[] avgTimes = {0, 0};
        System.out.println("press enter to start");
        Scanner in = new Scanner(System.in);
        in.nextLine();*/
        for(;;)
        {
            int[] arr1 = generateRandomArray(MIN_LENGTH, MAX_LENGTH, MIN_VAL, MAX_VAL);
            int[] arr2 = Arrays.copyOf(arr1, arr1.length);
            long startTime = System.nanoTime();
            quicksortNormal(arr1, 0, arr1.length-1);
            long endTime = System.nanoTime();
            long duration = endTime-startTime;
            System.out.print("normal: "+duration+" ns\t");
            startTime = System.nanoTime();
            quickSortTailRecurse(arr2, 0, arr2.length-1);
            endTime = System.nanoTime();
            duration = endTime-startTime;
            System.out.println("special: "+duration+" ns\tlength: "+arr1.length);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                Logger.getLogger(QuicksortCompare.class.getName()).log(Level.SEVERE, null, ex);
            }
        }    }


    public static int[] generateRandomArray(int minLength, int maxLength, int minVal, int maxVal)
    {
        int[] arr = new int[minLength+rand.nextInt((maxLength-minLength)+1)];
        populateArray(arr, minVal, maxVal);
        return arr;
    }

    public static void populateArray(int arr[], int min, int max)
    {
        for(int i = 0; i < arr.length; i++)
            arr[i] = min+rand.nextInt((max-min)+1);
    }

    private static void quickSortTailRecurse(int[] arr, int lo, int hi){
        if(lo >= hi) return;

        int p = partition(arr, lo, hi);

        if((p - lo ) <= (hi-p)){
          quickSortTailRecurse(arr, lo, p);
          quickSortTailRecurse(arr, p+1, hi);
        }else {
          quickSortTailRecurse(arr, p+1, hi);
          quickSortTailRecurse(arr, lo, p);
        }
      }

    public static void quicksortNormal(int[] a, int p, int r)
    {
        if(p<r)
        {
            int q = partition(a,p,r);
            quicksortNormal(a,p,q);
            quicksortNormal(a,q+1,r);
        }
    }

    public static void quicksortSmallSide_old(int[] a, int p, int r)
    {
        while(p<r)
        {
            int q = partition(a,p,r);
            if(q-p < r-q)
            {
                quicksortSmallSide(a,p,q);//may supposed to be q-1
                p = q+1;
            }
            else
            {
                quicksortSmallSide(a,q+1,r);
                r = q-1;
            }
        }
    } 

    private static int partition(int[] a, int p, int r) {

        int x = a[p];
        int i = p-1 ;
        int j = r+1 ;

        while (true) {
            i++;
            while ( i< r && a[i] < x)
                i++;
            j--;
            while (j>p && a[j] > x)
                j--;

            if (i < j)
                swap(a, i, j);
            else
                return j;
        }
    }

    private static void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

}
Java不能保证尾部调用优化

JVM实现仍然可以做到这一点,但Java代码不能依赖它来保证正确性

Java不能保证尾部调用优化



JVM实现仍然可以做到这一点,但Java代码不能依赖它来保证正确性

TCO并不意味着它会运行得更快或更慢;这意味着(在可以应用的情况下)执行不会导致堆栈溢出。需要10到1000个递归级别。。没有什么。尝试10万件物品。结果还是一样吗?如果递归版本无法运行,则TCO可能不起作用。@user2864740您所说的有道理,但如果没有进行尾调用优化,您如何解释更快的速度(因为还有1条If语句,它应该运行得稍慢)?此外,由于它是快速排序,请向它提供退化数据(即,当使用第一个元素作为分区时,对数据进行排序)以使其达到最坏情况,从而达到最大递归级别-如果数据随机化,100000项仅为~17次递归调用,这很难说明问题。第一步是确定TCO是否为再次,我不相信所显示的测试是这种情况的一个很好的指示器,实际上可能会导致非常浅的递归深度是一个反复无常的野兽。
QuickPorttailRecurse
QuickPortNormal
有何不同,它们不是都有一个非尾部递归调用,后跟一个尾部递归调用吗?TCO并不意味着它会运行得更快或更慢;它意味着(在可以应用的地方)执行不会导致堆栈溢出。10到1000个递归级别是..没什么。尝试100000个项目。结果是否仍然相同?如果递归版本无法运行,则TCO可能不起作用。@user2864740您所说的有意义,但如果没有发生尾调用优化,您如何解释更快的速度(因为还有一个if语句,所以它应该运行得稍微慢一点)?另外,因为它是快速排序的,所以向它提供退化数据(即,当使用第一个元素作为分区时,为它提供排序数据)因此,它达到了最坏的情况,从而达到了最大的递归级别—如果数据是随机的,100000个项目只有~17个递归调用,这很难说明问题。第一步是确定TCO是(还是不是)再次,我不相信所显示的测试是这种情况的一个很好的指示器,实际上可能会导致非常浅的递归深度是一只易变的野兽。
QuickPorttailRecurse
QuickPorttailRecurse
有何不同,它们不是都有一个非尾部递归调用,后面跟着一个尾部递归调用吗?OSX上的热点可以做到吗?我正在阅读代码,
QuickPorttailRecurse
QuickPorttailRecurse
都是尾部递归的,后者是尾部递归的首先是最小部分的尾部递归。你在这里看到的效果可能是由于缓存,而不是TCO。哦,谢谢你,我遵循了一个指南,但是这个例子有缺陷,所以可能没有尾部递归(我应该这么说)。因此,首先对较小的阵列进行排序具有更好的缓存命中率?您是如何计算的?您正在运行非常小的基准测试,中间有休眠。由于进程刚刚重新开始调度,第一个基准测试将有更多未命中,而第二个基准测试将受益于第一个基准测试首先运行。为获得更好的准确性,请重写测试:1.运行约一秒钟以预热,2.运行至少5秒钟,同时计时。“由于进程刚刚再次开始调度,第一个基准将有更多未命中”我不明白。我使用两个独立的数组来尝试避免任何缓存。这不起作用?那么你如何防止它呢?我让整个程序运行了大约5分钟,那么为什么你说“计时时至少运行5秒”?谢谢。OS X上的热点能做到这一点吗?我正在阅读代码,
QuickPortNormal
QuickPorttailRecurse
都是同样的尾部递归,后者只是在最小的部分首先进行尾部递归。您在这里看到的效果可能是由于缓存,而不是TCO。哦,谢谢,我遵循了一个指南,但示例中存在错误所以可能没有尾部递归(我应该这么说)。因此,首先对较小的阵列进行排序具有更好的缓存命中率?您是如何计算的?您正在运行非常小的基准测试,中间有休眠。由于进程刚刚重新开始调度,第一个基准测试将有更多未命中,而第二个基准测试将受益于第一个基准测试首先运行。为获得更好的准确性,请重写测试:1.运行约一秒钟以预热,2.运行至少5秒钟,同时计时。“由于进程刚刚再次开始调度,第一个基准将有更多未命中”我不明白。我使用两个独立的数组来尝试避免任何缓存。这不起作用?那么你如何防止它呢?我让整个程序运行了大约5分钟,那么你为什么说“计时时至少运行5秒”?谢谢。
run:
normal: 42000 ns    special: 39000 ns   length: 35
normal: 1240000 ns  special: 1202000 ns length: 829
normal: 336000 ns   special: 37000 ns   length: 63
normal: 358000 ns   special: 179000 ns  length: 839
normal: 102000 ns   special: 62000 ns   length: 322
normal: 72000 ns    special: 61000 ns   length: 393
normal: 11000 ns    special: 10000 ns   length: 75
normal: 26000 ns    special: 27000 ns   length: 210
normal: 134000 ns   special: 58000 ns   length: 337
normal: 91000 ns    special: 94000 ns   length: 393
normal: 66000 ns    special: 70000 ns   length: 551
normal: 107000 ns   special: 115000 ns  length: 805
normal: 54000 ns    special: 57000 ns   length: 386
normal: 21000 ns    special: 24000 ns   length: 197
normal: 29000 ns    special: 37000 ns   length: 250
normal: 117000 ns   special: 122000 ns  length: 932
normal: 199000 ns   special: 205000 ns  length: 963
normal: 31000 ns    special: 147000 ns  length: 148
normal: 16000 ns    special: 16000 ns   length: 136
normal: 193000 ns   special: 191000 ns  length: 959
normal: 107000 ns   special: 199000 ns  length: 634
objc[712]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/bin/java and /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined.
Profiler Agent: Waiting for connection on port 5140 (Protocol version: 15)
Profiler Agent: Established connection with the tool
Profiler Agent: Local accelerated session
normal: 674000 ns   special: 864000 ns  length: 450
normal: 24424000 ns special: 1798000 ns length: 186
normal: 3561000 ns  special: 2434000 ns length: 678
normal: 2112000 ns  special: 2148000 ns length: 908
normal: 1595000 ns  special: 1582000 ns length: 739
normal: 2179000 ns  special: 2248000 ns length: 936
normal: 1025000 ns  special: 997000 ns  length: 447
normal: 1185000 ns  special: 1161000 ns length: 574
normal: 1507000 ns  special: 2678000 ns length: 741
...
normal: 1554000 ns  special: 1534000 ns length: 656
normal: 366000 ns   special: 318000 ns  length: 152
normal: 138000 ns   special: 132000 ns  length: 67
normal: 1146000 ns  special: 1095000 ns length: 478
BUILD STOPPED (total time: 5 minutes 33 seconds)