多线程快速排序或合并排序

时间:2010-02-05 20:27:37

标签: java multithreading sorting quicksort mergesort

如何为Java实现并发快速排序或合并排序算法?

我们在16-(虚拟)内核Mac上遇到过问题,其中只有一个内核(!)使用默认的Java排序算法工作,而且很好地看到非常好的机器完全未被充分利用。所以我们写了自己的(我写的),我们确实获得了很好的加速(我编写了一个多线程的快速排序,由于它的分区性质,它很好地并行化,但我也可以编写一个mergesort)...但是我的实现只能扩展最多4个线程,它是专有代码,我宁愿使用来自信誉良好的源代码而不是使用我重新发明的轮子。

我在Web上找到的唯一一个例子是如何用Java编写多线程快速排序,它是忙碌循环(这真的很糟糕)使用:

while (helpRequested) { }

http://broadcast.oreilly.com/2009/06/may-column-multithreaded-algor.html

因此,除了无缘无故地丢失一个线程之外,它确保通过在while循环中忙碌循环来杀死perfs(这是令人难以置信的)。

因此我的问题:您是否知道Java中任何正确的多线程快速排序或mergesort实现来自信誉良好的来源?

我强调的事实是,我知道复杂性保持为O(n log n),但我仍然非常喜欢看到所有这些核心开始工作而不是空闲。请注意,对于其他任务,在相同的16个虚拟核心Mac上,我通过并行化代码看到了高达x7的加速(而且我并不是并发专家)。

所以即使很难复杂性保持O(n log n),我也非常欣赏x7或x8甚至x16加速。

9 个答案:

答案 0 :(得分:20)

尝试fork/join framework by Doug Lea

public class MergeSort extends RecursiveAction {
    final int[] numbers;
    final int startPos, endPos;
    final int[] result;

    private void merge(MergeSort left, MergeSort right) {
        int i=0, leftPos=0, rightPos=0, leftSize = left.size(), rightSize = right.size();
        while (leftPos < leftSize && rightPos < rightSize)
            result[i++] = (left.result[leftPos] <= right.result[rightPos])
                ? left.result[leftPos++]
                : right.result[rightPos++];
        while (leftPos < leftSize)
            result[i++] = left.result[leftPos++];
        while (rightPos < rightSize)
        result[i++] = right.result[rightPos++];
    }

    public int size() {
        return endPos-startPos;
    }

    protected void compute() {
        if (size() < SEQUENTIAL_THRESHOLD) {
            System.arraycopy(numbers, startPos, result, 0, size());
            Arrays.sort(result, 0, size());
        } else {
            int midpoint = size() / 2;
            MergeSort left = new MergeSort(numbers, startPos, startPos+midpoint);
            MergeSort right = new MergeSort(numbers, startPos+midpoint, endPos);
            coInvoke(left, right);
            merge(left, right);
        }
    }
}

(来源:http://www.ibm.com/developerworks/java/library/j-jtp03048.html?S_TACT=105AGX01&S_CMP=LP

答案 1 :(得分:9)

Java 8提供java.util.Arrays.parallelSort,它使用fork-join框架并行排序数组。文档提供了有关当前实现的一些详细信息(但这些是非规范性说明):

  

排序算法是一种并行排序合并,它将数组分解为自身排序然后合并的子数组。当子阵列长度达到最小粒度时,使用适当的Arrays.sort方法对子阵列进行排序。如果指定数组的长度小于最小粒度,则使用适当的Arrays.sort方法对其进行排序。该算法需要的工作空间不大于原始数组的大小。 ForkJoin公共池用于执行任何并行任务。

列表似乎没有相应的并行排序方法(即使RandomAccess列表应该对排序很好),因此您需要使用toArray,对该数组进行排序,并将结果存储回列表中。 (我已经问了一个关于here的问题。)

答案 2 :(得分:7)

对此抱歉,但您要求的是不可能的。我相信有人提到排序是IO绑定的,它们很可能是正确的。来自IBM的Doug Lea的代码是一个很好的工作,但我相信它主要是作为如何编写代码的一个例子。如果你在他的文章中注意到他从未公布过它的基准,而是发布其他工作代码的基准,例如计算平均值和并行找到最大值。如果您使用通用合并排序,快速排序,使用加入叉池的Dougs合并排序,以及使用快速排序加入叉池编写的排序,那么这些基准是什么。您会看到Merge Sort最适合N或100或更低。快速排序为1000到10000,如果您有100000或更高,使用Join Fork Pool进行快速排序可以胜过其余部分。这些测试是运行30次的随机数阵列,以创建每个数据点的平均值,并在具有大约2 gig ram的四核上运行。下面我有快速排序的代码。这主要表明,除非你试图对一个非常大的数组进行排序,否则你应该避免尝试改进你的代码排序算法,因为并行的数组在小N上运行得很慢。

Merge Sort
10  7.51E-06
100 1.34E-04
1000    0.003286269
10000   0.023988694
100000  0.022994328
1000000 0.329776132


Quick Sort
5.13E-05
1.60E-04
7.20E-04
9.61E-04
0.01949271
0.32528383


Merge TP
1.87E-04
6.41E-04
0.003704411
0.014830678
0.019474009
0.19581768

Quick TP
2.28E-04
4.40E-04
0.002716065
0.003115251
0.014046681
0.157845389

import jsr166y.ForkJoinPool;
import jsr166y.RecursiveAction;

//  derived from
//  http://www.cs.princeton.edu/introcs/42sort/QuickSort.java.html
//  Copyright © 2007, Robert Sedgewick and Kevin Wayne.
//  Modified for Join Fork by me hastily. 
public class QuickSort {

    Comparable array[];
    static int limiter = 10000;

    public QuickSort(Comparable array[]) {
        this.array = array;
    }

    public void sort(ForkJoinPool pool) {
        RecursiveAction start = new Partition(0, array.length - 1);        
        pool.invoke(start);
    }

    class Partition extends RecursiveAction {

        int left;
        int right;

        Partition(int left, int right) {
            this.left = left;
            this.right = right;
        }

        public int size() {
            return right - left;
        }

        @SuppressWarnings("empty-statement")
        //void partitionTask(int left, int right) {
        protected void compute() {
            int i = left, j = right;
            Comparable tmp;
            Comparable pivot = array[(left + right) / 2];

            while (i <= j) {
                while (array[i].compareTo(pivot) < 0) {
                    i++;
                }
                while (array[j].compareTo(pivot) > 0) {
                    j--;
                }

                if (i <= j) {
                    tmp = array[i];
                    array[i] = array[j];
                    array[j] = tmp;
                    i++;
                    j--;
                }
            }


            Partition leftTask = null;
            Partition rightTask = null;

            if (left < i - 1) {
                leftTask = new Partition(left, i - 1);
            }
            if (i < right) {
                rightTask = new Partition(i, right);
            }

            if (size() > limiter) {
                if (leftTask != null && rightTask != null) {
                    invokeAll(leftTask, rightTask);
                } else if (leftTask != null) {
                    invokeAll(leftTask);
                } else if (rightTask != null) {
                    invokeAll(rightTask);
                }
            }else{
                if (leftTask != null) {
                    leftTask.compute();
                }
                if (rightTask != null) {
                    rightTask.compute();
                }
            }
        }
    }
}

答案 3 :(得分:1)

刚刚编写了上面的MergeSort并且性能非常差。

代码块是指“coInvoke(left,right);”但没有引用这个并用invokeAll替换它(左,右);

测试代码是:

MergeSort mysort = new MyMergeSort(array,0,array.length);
ForkJoinPool threadPool = new ForkJoinPool();
threadPool.invoke(mysort);

但由于表现不佳而不得不停止它。

我看到上面的文章差不多有一年了,现在情况可能已经改变了。

我发现替代文章中的代码可以使用:http://blog.quibb.org/2010/03/jsr-166-the-java-forkjoin-framework/

答案 4 :(得分:0)

您可能已经考虑过这一点,但是从较高级别查看具体问题可能会有所帮助,例如,如果您不对某个数组或列表进行排序,则使用传统方法同时对单个集合进行排序可能会更容易算法而不是尝试同时对单个集合进行排序。

答案 5 :(得分:0)

过去几天我一直在面对多线程排序问题。正如on this caltech slide所解释的那样,你可以通过简单地多线程处理划分的每个步骤并在明显数量的线程(划分数量)上征服方法来做到最好。我想这是因为虽然您可以使用机器的所有64个核心在64个线程上运行64个分区,但是4个分区只能在4个线程上运行,2个在2上运行,1个在1上运行,等等。您的机器未被充分利用的递归。

我昨晚发生了一个解决方案,这可能对我自己的工作有用,所以我会在这里发布。

Iff,排序函数的第一个标准是基于最大大小为s的整数,无论​​是实际的整数还是字符串中的char,这样整数或char完全定义了排序的最高级别,然后我认为这是一个非常快速(简单)的解决方案。只需使用该初始整数将您的排序问题划分为较小的排序问题,并使用您选择的标准单线程排序算法对这些问题进行排序。我认为,分为s类可以一次完成。在执行s独立排序之后没有合并问题,因为您已经知道类1中的所有内容在第2类之前排序,依此类推。

示例:如果您希望基于strcmp()进行排序,则使用字符串中的第一个char将数据分成256个类,然后在下一个可用线程上对每个类进行排序,直到它们全部完成为止。

这个方法充分利用了所有可用的核心,直到问题得到解决,我认为它很容易实现。我还没有实现它,所以我可能还没有找到它的问题。它显然不能用于浮点排序,对大型s来说效率低。它的性能也很大程度上取决于用于定义类的整数/字符的熵。

这可能是Fabian Steeg用较少的词语提出的建议,但我明确表示你可以在某些情况下从更大的类别创建多个较小的排序。

答案 6 :(得分:0)

import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class IQ1 {
    public static void main(String[] args) {
        // Get number of available processors
        int numberOfProcessors = Runtime.getRuntime().availableProcessors();
        System.out.println("Number of processors : " + numberOfProcessors);
        // Input data, it can be anything e.g. log records, file records etc
        long[][] input = new long[][]{
              { 5, 8, 9, 14, 20 },
              { 17, 56, 59, 80, 102 },
              { 2, 4, 7, 11, 15 },
              { 34, 37, 39, 45, 50 }
            };

        /* A special thread pool designed to work with fork-and-join task splitting
         * The pool size is going to be based on number of cores available 
         */
        ForkJoinPool pool = new ForkJoinPool(numberOfProcessors);
        long[] result = pool.invoke(new Merger(input,  0, input.length));

        System.out.println(Arrays.toString(result));
    }
    /* Recursive task which returns the result
     * An instance of this will be used by the ForkJoinPool to start working on the problem
     * Each thread from the pool will call the compute and the problem size will reduce in each call
     */
    static class Merger extends RecursiveTask<long[]>{
        long[][] input;
        int low;
        int high;

        Merger(long[][] input, int low, int high){
            this.input = input;
            this.low = low;
            this.high = high;
        }

        @Override
        protected long[] compute() {            
            long[] result = merge();
            return result;
        }

        // Merge
        private long[] merge(){
            long[] result = new long[input.length * input[0].length];
            int i=0;
            int j=0;
            int k=0;
            if(high - low < 2){
                return input[0];
            }
            // base case
            if(high - low == 2){
                long[] a = input[low];
                long[] b = input[high-1];
                result = mergeTwoSortedArrays(a, b);
            }
            else{
                // divide the problem into smaller problems
                int mid = low + (high - low) / 2;
                Merger first = new Merger(input, low, mid);
                Merger second = new Merger(input, mid, high);
                first.fork();
                long[] secondResult = second.compute();
                long[] firstResult = first.join();

                result = mergeTwoSortedArrays(firstResult, secondResult);
            }

            return result;
        }

        // method to merge two sorted arrays
        private long[] mergeTwoSortedArrays(long[] a, long[] b){
            long[] result = new long[a.length + b.length];
            int i=0;
            int j=0;
            int k=0;
                while(i<a.length && j<b.length){
                    if(a[i] < b[j]){
                        result[k] = a[i];
                        i++;
                    } else{
                        result[k] = b[j];
                        j++;
                    }
                    k++;
                }

                while(i<a.length){
                    result[k] = a[i];
                    i++;
                    k++;
                }

                while(j<b.length){
                    result[k] = b[j];
                    j++;
                    k++;
                }

        return result;
    }
    }
}

答案 7 :(得分:0)

合并排序最方便的多线程范例是fork-join范例。这是Java 8和更高版本提供的。以下代码演示了使用fork-join进行合并排序。

import java.util.*;
import java.util.concurrent.*;

public class MergeSort<N extends Comparable<N>> extends RecursiveTask<List<N>> {
    private List<N> elements;

    public MergeSort(List<N> elements) {
        this.elements = new ArrayList<>(elements);
    }

    @Override
    protected List<N> compute() {
        if(this.elements.size() <= 1)
            return this.elements;
        else {
            final int pivot = this.elements.size() / 2;
            MergeSort<N> leftTask = new MergeSort<N>(this.elements.subList(0, pivot));
            MergeSort<N> rightTask = new MergeSort<N>(this.elements.subList(pivot, this.elements.size()));

            leftTask.fork();
            rightTask.fork();

            List<N> left = leftTask.join();
            List<N> right = rightTask.join();

            return merge(left, right);
        }
    }

    private List<N> merge(List<N> left, List<N> right) {
        List<N> sorted = new ArrayList<>();
        while(!left.isEmpty() || !right.isEmpty()) {
            if(left.isEmpty())
                sorted.add(right.remove(0));
            else if(right.isEmpty())
                sorted.add(left.remove(0));
            else {
                if( left.get(0).compareTo(right.get(0)) < 0 )
                    sorted.add(left.remove(0));
                else
                    sorted.add(right.remove(0));
            }
        }

        return sorted;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        List<Integer> result = forkJoinPool.invoke(new MergeSort<Integer>(Arrays.asList(7,2,9,10,1)));
        System.out.println("result: " + result);
    }
}

虽然不那么直接,但是下面的代码变种消除了ArrayList的过多复制。初始未排序列表仅创建一次,并且对子列表的调用无需自己执行任何复制。每次算法派生之前,我们都要复制数组列表。而且,现在,在合并列表而不是创建新列表并在其中复制值时,我们每次重用左侧列表并将值插入其中。通过避免多余的复制步骤,我们可以提高性能。我们在这里使用LinkedList是因为与ArrayList相比,插入操作相当便宜。我们也消除了对remove的调用,这在ArrayList上也很昂贵。

import java.util.*;
import java.util.concurrent.*;

public class MergeSort<N extends Comparable<N>> extends RecursiveTask<List<N>> {
    private List<N> elements;

    public MergeSort(List<N> elements) {
        this.elements = elements;
    }

    @Override
    protected List<N> compute() {
        if(this.elements.size() <= 1)
            return new LinkedList<>(this.elements);
        else {
            final int pivot = this.elements.size() / 2;
            MergeSort<N> leftTask = new MergeSort<N>(this.elements.subList(0, pivot));
            MergeSort<N> rightTask = new MergeSort<N>(this.elements.subList(pivot, this.elements.size()));

            leftTask.fork();
            rightTask.fork();

            List<N> left = leftTask.join();
            List<N> right = rightTask.join();

            return merge(left, right);
        }
    }

    private List<N> merge(List<N> left, List<N> right) {
        int leftIndex = 0;
        int rightIndex = 0;
        while(leftIndex < left.size() || rightIndex < right.size()) {
            if(leftIndex >= left.size())
                left.add(leftIndex++, right.get(rightIndex++));
            else if(rightIndex >= right.size())
                return left;
            else {
                if( left.get(leftIndex).compareTo(right.get(rightIndex)) < 0 )
                    leftIndex++;
                else
                    left.add(leftIndex++, right.get(rightIndex++));
            }
        }

        return left;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        List<Integer> result = forkJoinPool.invoke(new MergeSort<Integer>(Arrays.asList(7,2,9,-7,777777,10,1)));
        System.out.println("result: " + result);
    }
}

我们还可以通过使用迭代器而不是在执行合并时直接调用get来进一步改进代码。这样做的原因是按索引访问LinkedList的时间性能(线性)很差,因此使用迭代器可以消除在每个get上内部迭代链表所导致的速度下降。在迭代器上对next的调用是固定时间,而不是线性时间。下面的代码已修改为使用迭代器。

import java.util.*;
import java.util.concurrent.*;

public class MergeSort<N extends Comparable<N>> extends RecursiveTask<List<N>> {
    private List<N> elements;

    public MergeSort(List<N> elements) {
        this.elements = elements;
    }

    @Override
    protected List<N> compute() {
        if(this.elements.size() <= 1)
            return new LinkedList<>(this.elements);
        else {
            final int pivot = this.elements.size() / 2;
            MergeSort<N> leftTask = new MergeSort<N>(this.elements.subList(0, pivot));
            MergeSort<N> rightTask = new MergeSort<N>(this.elements.subList(pivot, this.elements.size()));

            leftTask.fork();
            rightTask.fork();

            List<N> left = leftTask.join();
            List<N> right = rightTask.join();

            return merge(left, right);
        }
    }

    private List<N> merge(List<N> left, List<N> right) {
        ListIterator<N> leftIter = left.listIterator();
        ListIterator<N> rightIter = right.listIterator();
        while(leftIter.hasNext() || rightIter.hasNext()) {
            if(!leftIter.hasNext()) {
                leftIter.add(rightIter.next());
                rightIter.remove();
            }
            else if(!rightIter.hasNext())
                return left;
            else {
                N rightElement = rightIter.next();
                if( leftIter.next().compareTo(rightElement) < 0 )
                    rightIter.previous();
                else {
                    leftIter.previous();
                    leftIter.add(rightElement);
                }
            }
        }

        return left;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        List<Integer> result = forkJoinPool.invoke(new MergeSort<Integer>(Arrays.asList(7,2,9,-7,777777,10,1)));
        System.out.println("result: " + result);
    }
}

最后是最复杂的代码版本,此迭代使用完全原位操作。仅创建初始ArrayList,并且从未创建其他集合。因此,逻辑很难遵循(因此我将其保存为最后)。但应尽可能接近理想的实现。

import java.util.*;
import java.util.concurrent.*;

public class MergeSort<N extends Comparable<N>> extends RecursiveTask<List<N>> {
    private List<N> elements;

    public MergeSort(List<N> elements) {
        this.elements = elements;
    }

    @Override
    protected List<N> compute() {
        if(this.elements.size() <= 1)
            return this.elements;
        else {
            final int pivot = this.elements.size() / 2;
            MergeSort<N> leftTask = new MergeSort<N>(this.elements.subList(0, pivot));
            MergeSort<N> rightTask = new MergeSort<N>(this.elements.subList(pivot, this.elements.size()));

            leftTask.fork();
            rightTask.fork();

            List<N> left = leftTask.join();
            List<N> right = rightTask.join();

            merge(left, right);
            return this.elements;
        }
    }

    private void merge(List<N> left, List<N> right) {
        int leftIndex = 0;
        int rightIndex = 0;
        while(leftIndex < left.size() ) {
            if(rightIndex == 0) {
                if( left.get(leftIndex).compareTo(right.get(rightIndex)) > 0 ) {
                    swap(left, leftIndex++, right, rightIndex++);
                } else {
                    leftIndex++;
                }
            } else {
                if(rightIndex >= right.size()) {
                    if(right.get(0).compareTo(left.get(left.size() - 1)) < 0 )
                        merge(left, right);
                    else
                        return;
                }
                else if( right.get(0).compareTo(right.get(rightIndex)) < 0 ) {
                    swap(left, leftIndex++, right, 0);
                } else {
                    swap(left, leftIndex++, right, rightIndex++);
                }
            }
        }

        if(rightIndex < right.size() && rightIndex != 0)
            merge(right.subList(0, rightIndex), right.subList(rightIndex, right.size()));
    }

    private void swap(List<N> left, int leftIndex, List<N> right, int rightIndex) {
        //N leftElement = left.get(leftIndex);
        left.set(leftIndex, right.set(rightIndex, left.get(leftIndex)));
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        List<Integer> result = forkJoinPool.invoke(new MergeSort<Integer>(new ArrayList<>(Arrays.asList(5,9,8,7,6,1,2,3,4))));
        System.out.println("result: " + result);
    }
}

答案 8 :(得分:-4)

为什么你认为并行排序会有所帮助?我认为大多数排序都是i / o绑定,而不是处理。除非您的比较进行了大量计算,否则不太可能加速。