Параллельное матричное умножение в java - PullRequest
0 голосов
/ 21 февраля 2020

Я пытаюсь реализовать матричное умножение с несколькими потоками. Кажется, все работает правильно, однако работает намного медленнее, чем обычный алгоритм. Вот мой код

public class Main {
    private static int nRows = 500; //number of rows and columns in matrices
    private static int[][] matrix1 = new int[nRows][nRows]; //first matrix for multiplication
    private static int[][] matrix2 = new int[nRows][nRows]; //second matrix for multiplication
    private static int[][] result1 = new int[nRows][nRows]; //result from linear matrix multiplication
    private static int[][] result2 = new int[nRows][nRows]; //result from parallel matrix multiplication

    private static Thread[][] pool = new Thread[nRows][nRows]; //array of threads

    //method used for transposing a matrix to get its column easily
    public static int[][] transpose(int[][] matrix){
        int[][] newMatrix = new int[matrix[0].length][matrix.length];
        for(int i=0;i<matrix[0].length;i++){
            for(int j=0;j<matrix.length;j++){
                newMatrix[i][j] = matrix[j][i];
            }
        }
        return newMatrix;
    }

    public static void main(String[] args){
        //initializing input matrices (setting all elements = 1)
        for(int i=0;i<nRows;i++){
            for(int j=0;j<nRows;j++){
                matrix1[i][j] = 1;
                matrix2[i][j] = 1;
            }
        }

        long start;
        long end;

        System.out.println("Linear algorithm");
        start = System.currentTimeMillis();

        //linear multiplication algorithm
        for(int i=0;i<nRows;i++){
            for(int j=0;j<nRows;j++){
                int temp = 0;
                for(int k=0;k<nRows;k++){
                    temp += matrix1[i][k] * matrix2[k][j];
                }
                result1[i][j] = temp;
            }
        }

        //show result
//        for(int i=0;i<nRows;i++){
//            for(int j=0;j<nRows;j++){
//                System.out.print(result1[i][j] + " ");
//            }
//            System.out.println();
//        }

        end = System.currentTimeMillis();
        System.out.println("Time with linear algorithm: " + (end - start));

        //--------------------

        System.out.println("Parallel algorithm");
        start = System.currentTimeMillis();

        int[][] matrix3 = transpose(matrix2); //get a transpose copy of second matrix

        for(int i=0;i<nRows;i++){
            for(int j=0;j<nRows;j++){
                pool[i][j] = new myThread(matrix1[i], matrix3[j], i, j); //creating a thread for each element
                pool[i][j].start(); //starting a thread
            }
        }

        for(int i=0;i<nRows;i++){
            for(int j=0;j<nRows;j++){
                try {
                    pool[i][j].join(); //waiting for the thread to finish its job
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        //show the result
//        for(int i=0;i<nRows;i++){
//            for(int j=0;j<nRows;j++){
//                System.out.print(result2[i][j] + " ");
//            }
//            System.out.println();
//        }

        end = System.currentTimeMillis();
        System.out.println("Time with parallel algorithm: " + (end - start));
    }

    //class, where parallel multiplication is implemented
    private static class myThread extends Thread{
        private int[] row = new int[nRows]; //row for multiplication
        private int[] col = new int[nRows]; //column for multiplication
        private int i;  //row index of the element in resulting matrix
        private int j; //column index of the element in resulting matrix

        //constructor
        public myThread(int[] r, int[] c, int i, int j){
            row = r;
            col = c;
            this.i = i;
            this.j = j;
        }

        public void run(){
            int temp = 0;
            for(int k=0;k<nRows;k++){
                temp += row[k] * col[k]; //getting the element by multiplying row and column of two matrices
            }
            result2[i][j] = temp; //writing the resulting element to the resulting matrix
        }
    }
}

Здесь я создаю новый поток для каждого элемента в результирующей матрице. Затем я записываю эти потоки в массив, запускаю их и, наконец, жду, пока они не закончат работу. Я видел несколько реализаций, где вся входная матрица (обе) была бы задана в качестве параметров потока. Однако моя задача состоит в том, чтобы придумать алгоритм, в котором указаны только одна строка и один столбец (необходимые для этого конкретного элемента).

После измерения прошедшего времени я получаю следующие результаты

Linear algorithm
Time with linear algorithm: 557
Parallel algorithm
Time with parallel algorithm: 38262

Что я делаю не так? Заранее спасибо!

Ответы [ 2 ]

0 голосов
/ 21 февраля 2020

Параллельная обработка хорошо работает только с обильными процессорами. Если у вас недостаточно процессоров, чтобы разделить работу, и процессоров, достаточных для обработки нагрузки, то распараллеливание не выбьет ваши носки. Распараллеливание может замедлить обработку.

Если у вас P процессоров и 8 (P) одновременных запросов, то использование одного потока на запрос часто более эффективно для пропускной способности. Точка безубыточности для разложения где-то чуть больше 8 (P), в зависимости от применения. Логика c здесь проста. Если у вас есть P-процессоры, и вы соответственно распределяете свою работу, но перед вами стоят сотни других задач, то какой смысл разбивать? Может быть быстрее обрабатывать каждый запрос последовательно.

Умножение матриц - это настоящая проблема с памятью. Без достаточного количества памяти распараллеливание может замедлить обработку.

Тем не менее, хороший способ разбить и объединить слишком длинный, чтобы его здесь дублировать. Я поддерживаю продукт с открытым исходным кодом «разделяй и властвуй» , в котором матрица умножается в качестве одной из встроенных функций. Посмотри и сделай что можешь.

0 голосов
/ 21 февраля 2020

Код, который вы написали, будет хорошо работать на графическом процессоре, где концепция потоков сильно отличается, а накладные расходы в основном равны нулю. В системах на основе процессора порождение потоков является исключительно медленной операцией, и это имеет смысл, только если вы можете амортизировать эти накладные расходы из-за большого вычислительного объема работы.

Вот несколько общих советов, которые помогут поможет вам лучше написать параллельные алгоритмы для процессоров:

  • При выполнении сложных вычислительных задач используйте столько потоков, сколько имеется физических исполнительных блоков (ядер). Методы SMT, такие как HyperThreading, мало помогают, если не много задержки в памяти. Для небольших матриц, которые помещаются в кэш-память ЦП L1 и L2, задержка очень мала, и SMT нечего выиграть. Когда более одного потока совместно используют одно и то же ядро, ОС вынуждена переключаться между двумя контекстами, что увеличивает издержки и может sh кэшировать.
  • Сохранять степень детализации распараллеливания настолько грубой, насколько это возможно, чтобы максимизировать работу за нитку. Вместо того, чтобы иметь одну операцию строки x столбца на поток, каждый поток должен работать с непрерывными блоками строк / столбцов. Вы можете попробовать и распараллелить только внешнюю l oop, т. Е. Только по строкам первой матрицы.
  • Сохраняйте количество потоков в зависимости от аппаратных свойств (количество ядер) и независимо от проблемы размер. Создание отдельного потока для каждой строки и столбца линейно масштабирует накладные расходы в зависимости от размера проблемы, что очень плохо с точки зрения производительности.
  • Избегайте ложного совместного использования. Это происходит, когда два или более потоков, работающих на разных ядрах, записывают в области памяти, находящиеся в одной строке кэша. Когда один поток обновляет кэш своего ядра, это изменение распространяет и делает недействительными кэш-память других ядер, имеющих одинаковую строку кэша, заставляя их повторно загружать данные. В вашем случае 16 последовательных значений result2 попадают в одну и ту же строку кэша (строки кэша в x86 и ARM имеют длину 64 байта, int составляет 4 байта) и записываются 16 различными потоками. Использование временной переменной суммирования как-то облегчает эту проблему - она ​​гораздо серьезнее, когда ложное совместное использование происходит многократно во внутреннем (-мощном) l oop.
  • Использование пулов потоков для повторяющихся задач, когда число из рабочих элементов превышает количество потоков, и каждый поток будет работать многократно. В вашем случае вы предоставляете каждому потоку отдельный рабочий элемент, так что это на самом деле не объединяет.

В итоге, запустите столько потоков, сколько физических ядер, и пусть они работают на больших непрерывных порциях ввода матрицы.

...