簡體   English   中英

Java fork-join 性能

[英]Java fork-join Performance

我有 Merge-Sort 的示例實現,一個使用 Fork-Join,另一個是直接遞歸 function。

看起來 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);
    }
}

恕我直言,主要原因不是由於線程產生和匯集而產生的開銷。

我認為多線程版本運行緩慢主要是因為您不斷創建新陣列數百萬次。 最終,您使用單個元素創建了100萬個數組,這對垃圾收集器來說是一個令人頭痛的問題。

所有DivideTask都可以在陣列的不同部分(兩半)上運行,因此只需向它們發送一個范圍並讓它們在該范圍內運行。

此外,您的並行化策略使得無法使用聰明的“輔助數組”優化(注意順序版本中的helper數組)。 這種優化將“輸入”數組與一個“輔助”數組進行交換,在該數組上進行合並,因此不應為每個合並操作創建一個新數組:一種節省內存的技術,如果你不這樣做就不能做t 按遞歸樹的級別並行化。

對於一個類作品,我不得不並行化MergeSort,並通過遞歸樹的級別並行化來設法獲得一個很好的加速。 不幸的是,代碼在C中並使用OpenMP。 如果你想我可以提供它。

正如gd1指出的那樣,你正在進行大量的數組分配和復制; 這將花費你。 您應該在同一個數組的不同部分上工作,注意沒有子任務在另一個子任務正在處理的部分上工作。

但除此之外,fork / join方法帶來了一定的開銷(與任何並發一樣)。 事實上,如果你看一下RecursiveTask的javadocs,他們甚至會指出他們的簡單例子會表現得很慢,因為分叉過於細化。

長話短說,你應該有更少的細分,每個細分做得更多。 更一般地說,只要你擁有比核心更多的非阻塞線程,吞吐量就不會有所改善,實際上開銷會開始削減它。

在不深入研究代碼的情況下,生成新線程代價高昂。 如果你沒有太多的工作要做,那么僅僅出於性能原因它是不值得的。 非常普遍地在這里談論,但是在生成新線程並開始運行之前,單個線程可以循環數千次(特別是在Windows上)。

請參閱Doug Lea的論文 (在2. DESIGN下),他說:

“但是,java.lang.Thread類(以及Java線程通常基於POSIX pthreads)是支持fork / join程序的次優工具”

還發現了有關使用Fork / Join Dan Grossman的Fork / Join簡介的以下信息

我遇到了同樣的問題。 在我的合並排序實現中,我只復制可能比左邊部分短的右邊部分。 此外,我在復制和合並時跳過了右側部分可能的最大元素。 即使進行了優化,並行實現仍然比迭代實現慢。 根據 Leetcode,我的迭代方法比 91.96% 快,我的並行實現比 56.59% 快。

  1. https://leetcode.com/problems/sort-an-array/submissions/878182467/ https://leetcode.com/problems/sort-an-array/submissions/877801578/
import java.util.concurrent.RecursiveAction;

class Solution {

    public static class Sort extends RecursiveAction {
        private int[] a;
        private int left;
        private int right;

        public Sort(int[] a, int left, int right) {
            this.a = a;
            this.left = left;
            this.right = right;
        }

        @Override
        protected void compute() {
            int m = (left + right) / 2;
            if (m >= left + 1) {
                Sort leftHalf = new Sort(a, left, m);
                leftHalf.fork();
                Sort rightHalf = new Sort(a, m+1, right);
                rightHalf.compute();
                leftHalf.join();
            }
            merge(a, left, right, m);
        }

        private void merge(int[] a, int left, int right, int mid) {
            if (left == right || left + 1 == right && a[left] <= a[right])
                return;
            // let l point to last element of left half, r point to last element of right half
            int l = mid, r = right;
            // skip possible max elements
            while (l < r && a[l] <= a[r])
                r -= 1;
            // size of remaining right half
            int size = r-l;
            int[] buf = new int[size];
            for (int i = 0; i < size; i++){
                buf[i] = a[mid + 1 + i];
            }
            int i = size-1;
            while (i >= 0) {
                if (l >= left && a[ l] >buf[i]) {
                    a[r] = a[l];
                    l -= 1;
                } else {
                    a[r] = buf[i];
                    i -= 1;
                }
                r -= 1;
            }
        }
    }

    public int[] sortArray(int[] a) {
        ForkJoinPool threadPool = ForkJoinPool.commonPool();
        threadPool.invoke(new Sort(a, 0, a.length-1));
        return a;
    }
}

Iterative implementation:
class Solution {
    public int[] sortArray(int[] a) {
        int[] buf = new int[a.length];
        int size = 1;
        while (size < a.length) {
            int left = 0 + size - 1;
            int right = Math.min(left + size, a.length-1);
            while (left < a.length) {
                merge(a, size, left, right, buf);
                left += 2 * size;
                right = Math.min(left + size, a.length-1);
            }
            size *= 2;
        }
        return a;
    }

    private void merge(int[] a, int size, int l, int r, int[] buf) {
        int terminal1 = l - size;
        int right = r;
        while (l < right && a[l] <= a[right])
            right--;
        r = right;
        int rsize = right - l;
        for (int i = rsize-1; i >= 0; i--) {
            buf[i] = a[right--];
        }
        int i = rsize-1;
        while (i >= 0) {
            if (l > terminal1 && a[l] > buf[i]) {
                a[r--] = a[l--];
            } else {
                a[r--] = buf[i--];
            }
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM