2

Merge-Sort のサンプル実装があります。1 つは Fork-Join を使用し、もう 1 つは単純な再帰関数です。

fork-join は単純な再帰よりも遅いようですが、なぜですか?

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

class DivideTask extends RecursiveTask<int[]> {
    private static final long serialVersionUID = -7017440434091885703L;
    int[] arrayToDivide;

    public DivideTask(int[] arrayToDivide) {
        this.arrayToDivide = arrayToDivide;
    }

    @Override
    protected int[] compute() {
        //List<RecursiveTask> forkedTasks = new ArrayList<>();

        /*
         * We divide the array till it has only 1 element. 
         * We can also custom define this value to say some 
         * 5 elements. In which case the return would be
         * Arrays.sort(arrayToDivide) instead.
         */
        if (arrayToDivide.length > 1) {

            List<int[]> partitionedArray = partitionArray();

            DivideTask task1 = new DivideTask(partitionedArray.get(0));
            DivideTask task2 = new DivideTask(partitionedArray.get(1));
            invokeAll(task1, task2);

            //Wait for results from both the tasks
            int[] array1 = task1.join();
            int[] array2 = task2.join();

            //Initialize a merged array
            int[] mergedArray = new int[array1.length + array2.length];

            mergeArrays(task1.join(), task2.join(), mergedArray);

            return mergedArray;
        }
        return arrayToDivide;
    }

    private void mergeArrays(int[] array1, int[] array2, int[] mergedArray) {

        int i = 0, j = 0, k = 0;

        while ((i < array1.length) && (j < array2.length)) {

            if (array1[i] < array2[j]) {
                mergedArray[k] = array1[i++];
            } else {
                mergedArray[k] = array2[j++];
            }

            k++;
        }

        if (i == array1.length) {
            for (int a = j; a < array2.length; a++) {
                mergedArray[k++] = array2[a];
            }
        } else {
            for (int a = i; a < array1.length; a++) {
                mergedArray[k++] = array1[a];
            }
        }
    }

    private List<int[]> partitionArray() {
        int[] partition1 = Arrays.copyOfRange(arrayToDivide, 0, arrayToDivide.length / 2);

        int[] partition2 = Arrays.copyOfRange(arrayToDivide, arrayToDivide.length / 2, arrayToDivide.length);
        return Arrays.asList(partition1, partition2);
    }
}

public class ForkJoinTest {
    static int[] numbers;
    static final int SIZE = 1_000_000;
    static final int MAX = 20;

    public static void main(String[] args) {
        setUp();

        testMergeSortByFJ();
        testMergeSort();
    }

    static void setUp() {
        numbers = new int[SIZE];
        Random generator = new Random();
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = generator.nextInt(MAX);
        }
    }

    static void testMergeSort() {
        long startTime = System.currentTimeMillis();

        Mergesort sorter = new Mergesort();
        sorter.sort(numbers);

        long stopTime = System.currentTimeMillis();
        long elapsedTime = stopTime - startTime;
        System.out.println("Mergesort Time:" + elapsedTime + " msec");
    }

    static void testMergeSortByFJ() {
        //System.out.println("Unsorted array: " + Arrays.toString(numbers));
        long t1 = System.currentTimeMillis();
        DivideTask task = new DivideTask(numbers);
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        forkJoinPool.invoke(task);
        //System.out.println("Sorted array: " + Arrays.toString(task.join()));
        System.out.println("Fork-Join Time:" + (System.currentTimeMillis() - t1) + " msec");
    }
 }

class Mergesort {
    private int[] msNumbers;
    private int[] helper;

    private int number;

    private void merge(int low, int middle, int high) {

        // Copy both parts into the helper array
        for (int i = low; i <= high; i++) {
            helper[i] = msNumbers[i];
        }

        int i = low;
        int j = middle + 1;
        int k = low;
        // Copy the smallest values from either the left or the right side back
        // to the original array
        while (i <= middle && j <= high) {
            if (helper[i] <= helper[j]) {
                msNumbers[k] = helper[i];
                i++;
            } else {
                msNumbers[k] = helper[j];
                j++;
            }
            k++;
        }
        // Copy the rest of the left side of the array into the target array
        while (i <= middle) {
            msNumbers[k] = helper[i];
            k++;
            i++;
        }

    }

    private void mergesort(int low, int high) {
        // Check if low is smaller then high, if not then the array is sorted
        if (low < high) {
            // Get the index of the element which is in the middle
            int middle = low + (high - low) / 2;
            // Sort the left side of the array
            mergesort(low, middle);
            // Sort the right side of the array
            mergesort(middle + 1, high);
            // Combine them both
            merge(low, middle, high);
        }
    }

    public void sort(int[] values) {
        this.msNumbers = values;
        number = values.length;
        this.helper = new int[number];
        mergesort(0, number - 1);
    }
}
4

4 に答える 4

5

私見の主な理由は、スレッドの生成とプーリングによるオーバーヘッドではありません。

マルチスレッド バージョンの実行速度が遅いのは、主に新しい配列を常に何百万回も継続的に作成しているためだと思います。最終的に、1 つの要素で 100 万個の配列を作成することになり、ガベージ コレクターにとって頭痛の種になります。

すべてDivideTaskの は、配列の異なる部分 (2 つの半分) で操作できるので、それらに範囲を送信して、その範囲で操作するようにします。

さらに、あなたの並列化戦略は巧妙な「ヘルパー配列」最適化を使用することを不可能にします (helper順次バージョンの配列に注目してください)。この最適化は、「入力」配列をマージが行われる「ヘルパー」配列と交換するため、マージ操作ごとに新しい配列が作成されることはありません。そうしないとできないメモリ節約テクニックです。 t再帰ツリーのレベルで並列化します

授業のために、MergeSort を並列化する必要がありましたが、再帰ツリーのレベルで並列化することで、うまくスピードアップすることができました。残念ながら、コードは C であり、OpenMP を使用しています。ご希望でしたらご用意できます。

于 2013-01-03T00:44:04.743 に答える
4

gd1 が指摘しているように、多くの配列の割り当てとコピーを行っています。これには費用がかかります。代わりに、別のサブタスクが作業しているセクションでサブタスクが機能しないように注意して、同じ 1 つの配列の異なるセクションで作業する必要があります。

しかし、それ以上に、fork/join アプローチにはある程度のオーバーヘッドが伴います (他の同時実行と同様)。実際、 RecursiveTaskの javadoc を見ると、分岐が細かすぎるため、単純な例の実行が遅いとさえ指摘されています。

簡単に言うと、サブディビジョンの数を減らして、それぞれのサブディビジョンがより多くのことを行う必要があります。より一般的には、ブロックされていないスレッドがコアよりも多い場合、スループットは改善されず、実際にはオーバーヘッドが徐々に減少し始めます。

于 2013-01-03T00:51:56.957 に答える
0

コードを詳しく調べないと、新しいスレッドを生成するのにコストがかかります。やるべき仕事があまりない場合、パフォーマンス上の理由だけでは価値がありません。ここでは非常に一般的な話ですが、新しいスレッドが生成されて実行を開始する前に、単一のスレッドが何千回もループする可能性があります (特に Windows の場合)。

Doug Lea の論文(2. DESIGN の下)を参照してください。彼は次のように述べています。

「しかし、java.lang.Thread クラス (および Java スレッドがしばしばベースとする POSIX pthreads) は、fork/join プログラムをサポートするための次善の手段です」

于 2013-01-03T00:38:54.290 に答える
0

Fork/Join の操作に関する次の情報も見つかりました 。Dan Grossman の Fork/Join の紹介

于 2013-01-03T23:36:18.570 に答える