web-dev-qa-db-fra.com

Tri rapide ou mergesort multithread

Comment puis-je implémenter un algorithme de tri rapide ou un algorithme mergesort simultané pour Java?

Nous avons eu des problèmes sur un Mac à 16 cœurs (virtuels) où un seul cœur (!) Fonctionnait avec l'algorithme de tri Java par défaut et il était, bien, peu agréable de voir cette très fine machine complètement sous-utilisée. Nous avons donc écrit le nôtre (je l'ai écrit) et nous avons effectivement obtenu de bonnes accélérations (j'ai écrit un tri rapide multithread et, en raison de sa nature partitionnante, il est très bien parallélisé, mais j'aurais aussi pu écrire un mergesort) ... ... jusqu'à 4 threads, c'est du code propriétaire, et je préférerais en utiliser un provenant d'une source réputée au lieu d'utiliser ma roue réinventée.

Le seul que j'ai trouvé sur le Web est un exemple de la façon dont not pour écrire un quicksort multi-thread en Java, c'est une boucle occupée (ce qui est vraiment terrible) en utilisant un:

while (helpRequested) { }

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

Donc, en plus de perdre un thread sans raison, il faut s'assurer de tuer les Perfs en effectuant une boucle occupée dans cette boucle while (ce qui est ahurissant).

D’où ma question: connaissez-vous une implémentation de tri rapide ou multithread correctement multithread en Java qui proviendrait d’une source réputée?

Je mets l'accent sur le fait que je sais que la complexité reste O (n log n) mais j'aimerais quand même beaucoup de voir tous ces cœurs fonctionner au lieu de tourner au ralenti. Notez que pour d’autres tâches, sur ce même Mac à 16 cœurs virtuels virtuels, j’ai vu une accélération jusqu’à x7 en parallélisant le code (et je ne suis nullement un expert en concurrence simultanée).

Donc même si la complexité reste O (n log n), j'apprécierais vraiment une accélération x7, x8 ou même x16.

25
SyntaxT3rr0r

essayer fork/rejoindre le framework par 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);
        }
    }
}

(source: http://www.ibm.com/developerworks/Java/library/j-jtp03048.html?S_TACT=105AGX01&S_CMP=LP )

20
dfa

Java 8 fournit Java.util.Arrays.parallelSort , qui trie les tableaux en parallèle à l’aide du framework fork-join. La documentation fournit des détails sur la mise en œuvre actuelle (mais il s’agit de notes non normatives):

L'algorithme de tri est une fusion de tri parallèle qui divise le tableau en sous-tableaux qui sont eux-mêmes triés puis fusionnés. Lorsque la longueur du sous-tableau atteint une granularité minimale, le sous-tableau est trié à l'aide de la méthode Arrays.sort appropriée. Si la longueur du tableau spécifié est inférieure à la granularité minimale, il est alors trié à l'aide de la méthode Arrays.sort appropriée. L'algorithme nécessite un espace de travail ne dépassant pas la taille du tableau d'origine. Le pool commun ForkJoin est utilisé pour exécuter toutes les tâches parallèles.

Il ne semble pas exister de méthode de tri parallèle correspondante pour les listes (même si RandomAccess lists devrait être sympa avec le tri), vous devez donc utiliser toArray, trier ce tableau et stocker le résultat dans la liste. . (J'ai posé une question à ce sujet ici .)

8
Jeffrey Bosboom

Désolé, mais ce que vous demandez n'est pas possible. Je crois que quelqu'un d'autre a mentionné que le tri est IO lié et qu'ils sont probablement corrects. Le code d’IBM de Doug Lea est un bel ouvrage mais j’estime qu’il s’agit surtout d’un exemple d’écriture de code. Si vous remarquez dans son article, il n'a jamais publié les repères pour cela, mais plutôt des repères pour d'autres codes de travail tels que le calcul des moyennes et la recherche du min max en parallèle. Voici quels sont les points de repère si vous utilisez un tri générique de fusion, un tri rapide, un tri de fusion de Dougs à l'aide d'un pool de jonction de branche et un scénario que j'ai rédigé à l'aide d'un pool de jonction de tri rapide. Vous verrez que le tri par fusion est le meilleur pour un N de 100 ou moins. Le tri rapide pour 1000 à 10000 et le tri rapide à l’aide d’un pool de fourches joignantes bat le reste si vous en avez 100 000 et plus. Ces tests consistaient en des ensembles de nombres aléatoires fonctionnant 30 fois pour créer une moyenne pour chaque point de données et fonctionnaient sur un quad core avec environ 2 Go de bélier. Et ci-dessous, j'ai le code pour le tri rapide. Cela montre généralement que, sauf si vous essayez de trier un très grand tableau, vous devriez éviter d'essayer d'améliorer votre algorithme de tri des codes car les parallèles fonctionnent très lentement sur de petits 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();
                }
            }
        }
    }
}
7
medv4380

Nous venons de coder le MergeSort ci-dessus et les performances étaient très médiocres.

Le bloc de code fait référence à "coInvoke (left, right);" mais il n'y avait aucune référence à cela et l'a remplacé par invokeAll (gauche, droite);

Le code de test est:

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

mais a dû l'arrêter en raison de mauvaises performances.

Je vois que l'article ci-dessus a presque un an et que les choses ont peut-être changé maintenant.

J'ai trouvé le code dans l'article alternatif au travail: http://blog.quibb.org/2010/03/jsr-166-the-Java-forkjoin-framework/

1
Graham Seed

J'ai moi-même été confronté au problème de tri multithread ces derniers jours. Comme expliqué sur cette diapositive caltech , le mieux que vous puissiez faire consiste simplement à effectuer une lecture multiple de chaque étape de la division et de vaincre les approches au-delà du nombre évident de threads (le nombre de divisions) est limité. Je suppose que ceci est dû au fait que bien que vous puissiez exécuter 64 divisions sur 64 threads en utilisant les 64 cœurs de votre machine, les 4 divisions ne peuvent être exécutées que sur 4 threads, le 2 sur 2 et le 1 sur 1, etc. de la récursivité de votre machine est sous-utilisée.

Une solution m'est venue hier soir qui pourrait être utile dans mon propre travail, alors je la posterai ici.

Si le premier critère de votre fonction de tri est basé sur un entier de taille s maximale, que ce soit un entier ou un caractère dans une chaîne, tel que cet entier ou caractère définisse entièrement le niveau le plus élevé de votre tri, alors je pense qu'il une solution très rapide (et facile). Utilisez simplement cet entier initial pour diviser votre problème de tri en problèmes de tri plus petits, et triez ceux-ci en utilisant le type de tri standard à un seul thread de votre choix. La division en classes peut être faite en un seul passage, je pense. Il n’ya pas de problème de fusion après avoir effectué les tris indépendants, parce que vous savez déjà que tout dans la classe 1 est trié avant la classe 2, et ainsi de suite.

Exemple: si vous souhaitez effectuer un tri basé sur strcmp (), utilisez le premier caractère de votre chaîne pour fractionner vos données en 256 classes, puis triez chaque classe sur le prochain thread disponible jusqu'à ce qu'elles soient toutes terminées.

Cette méthode utilise pleinement tous les cœurs disponibles jusqu'à la résolution du problème, et je pense que c'est facile à mettre en œuvre. Cependant, je ne l'ai pas encore implémenté, il y a donc peut-être des problèmes que je n'ai pas encore trouvés. Cela ne peut clairement pas fonctionner pour les tris flottants et serait inefficace pour les grands art. Ses performances seraient également fortement dépendantes de l'entropie du nombre entier/caractère utilisé pour définir les classes.

C'est peut-être ce que proposait Fabian Steeg en moins de mots, mais je tiens à préciser que vous pouvez créer plusieurs types plus petits à partir d'un type plus grand dans certaines circonstances.

0
Rob_before_edits
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;
    }
    }
}
0
Prakash Devta

Vous avez probablement envisagé cela, mais il pourrait être utile d'examiner le problème concret à un niveau supérieur. Par exemple, si vous ne triez pas un seul tableau ou une seule liste, il serait beaucoup plus facile de trier des collections individuelles simultanément à l'aide de l'algorithme traditionnel. essayer de trier simultanément une seule collection.

0
Fabian Steeg