【问题标题】:Parallel matrix multiplication in javajava中的并行矩阵乘法
【发布时间】:2020-02-20 23:17:04
【问题描述】:

我正在尝试用多个线程实现矩阵乘法。一切似乎都正常工作,但是,它的工作速度比通常的算法慢得多。这是我的代码

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

我做错了什么?提前致谢!

【问题讨论】:

  • 一些观察:1) 由于首先发生并行性需要开销,并行操作实际上最终变慢是很常见的。 2) 在 Java 中测量任何东西的性能要比对System.currentTimeMillis() 进行两次调用的差异要复杂得多。 stackoverflow.com/q/504103/869736
  • 使用并发对于阻塞 I/O 操作(如写入磁盘、调用数据库或 Web 服务)很有用,您所做的只是乘法,您必须考虑是否创建一个全新的线程(单次乘法)是值得的
  • 您正在为 500x500 矩阵中的每个元素启动一个线程?那么,25,000 个线程?我并不惊讶它变慢了。在具有 25,000 个内核和 1 TB 内存的机器上试一试。或者,更好的是,使用线程池执行器,其线程数与内核数一样多,并为其提供任务以进行单独的乘法运算。
  • 尝试注释掉并行实现中的所有乘法逻辑并再次运行它,您会注意到绝大多数时间只是在旋转所有线程。
  • 让每个线程处理(例如,矩阵的一行)可能会更好,以减少为此类小操作启动线程的开销。

标签: java multithreading parallel-processing matrix-multiplication


【解决方案1】:

您编写的代码可以在 GPU 上正常工作,其中线程的概念非常不同,开销基本上为零。在基于 CPU 的系统上,生成线程是一项异常缓慢的操作,只有在您能够将这种开销分摊到大量计算工作上时才有意义。

以下是一些通用建议,可帮助您为 CPU 编写更好的并行算法:

  • 对于计算量大的任务,使用与物理执行单元(内核)一样多的线程。除非存在大量内存延迟,否则超线程等 SMT 技术并没有太大帮助。对于适合 L1 和 L2 CPU 缓存的小型矩阵,延迟非常低,并且从 SMT 中没有任何好处。当多个线程共享同一个内核时,操作系统必须在两者之间进行上下文切换,这会增加开销并可能会破坏缓存。
  • 尽可能粗略地保持并行化粒度,以便最大化每个线程的工作量。不是每个线程进行一次行 x 列操作,而是让每个线程对连续的行/列块进行操作。您可以尝试仅并行化外循环,即仅在第一个矩阵的行上。
  • 保持线程数取决于硬件属性(内核数)并且与问题大小无关。为每一行和每一列生成一个单独的线程会使开销与问题大小成线性关系,从性能的角度来看,这确实很糟糕。
  • 避免虚假共享。当在不同内核上运行的两个或多个线程写入位于同一高速缓存行中的内存位置时,就会发生这种情况。当一个线程更新其内核的缓存时,更改会传播并使具有相同缓存行的其他内核的缓存无效,从而迫使它们重新获取数据。在您的情况下,result2 的 16 个连续值位于同一缓存行中(x86 和 ARM 上的缓存行长 64 个字节,int 为 4 个字节)并由 16 个不同的线程写入。临时求和变量的使用以某种方式缓解了这个问题——当错误共享在内部(最)循环中重复发生时,它会更加严重。
  • 当工作项的数量超过线程的数量并且每个线程将多次获得工作时,使用线程池进行重复任务。在您的情况下,您给每个线程一个单独的工作项,所以这并不是真正的池化。

总之,启动与物理内核一样多的线程,并让它们在输入矩阵的大块连续块上工作。

【讨论】:

    【解决方案2】:

    并行处理仅适用于大量处理器。如果您没有足够的处理器来拆分工作,也没有足够的处理器来处理负载,那么并行化不会让您大吃一惊。并行化可能会使处理速度变慢。

    如果您有 P 个处理器和 8(P) 个并发请求,那么每个请求使用一个线程通常会更有效地提高吞吐量。分解的盈亏平衡点刚好大于 8(P),具体取决于应用。这里的逻辑很简单。如果您有可用的 P 处理器,并且您相应地拆分了您的工作,但您面前还有数百个其他任务,那么拆分的意义何在?按顺序处理每个请求可能会更快。

    矩阵乘法是一个真正的内存消耗。如果没有足够的内存,并行化可能会使处理速度变慢。

    也就是说,拆分和连接的好方法太冗长,无法在此处复制。我维护了一个开源divide-and-conquer product,它具有矩阵乘法作为其内置函数之一。看看,做你可以做的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-03-08
      • 2012-01-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多