在java中,多线程矩阵加法比单线程版本耗时更长

在java中,多线程矩阵加法比单线程版本耗时更长,java,multithreading,concurrency,executorservice,Java,Multithreading,Concurrency,Executorservice,Java中的并发性让我的手变得脏兮兮的,并且在多线程处理中遇到了这个相当常见的问题。我有一段代码(如下所示),它只取两个矩阵m1和m2,并将m1[I][j]和m2[I][j]之和写入result[I][j] for(int i = 0; i < numCols ; i++) { for(int j = 0 ; j < numRows ; j++) { int finalI = i; int fin

Java中的并发性让我的手变得脏兮兮的,并且在多线程处理中遇到了这个相当常见的问题。我有一段代码(如下所示),它只取两个矩阵m1和m2,并将
m1[I][j]
m2[I][j]
之和写入
result[I][j]

for(int i = 0; i < numCols ; i++) {
            for(int j = 0 ; j < numRows ; j++) {
                int finalI = i;
                int finalJ = j;
                executorService.execute(
                        new Runnable() {
                            @Override
                            public void run()  {
                                    ArrayList<Integer> v1 = m1.get(finalI);
                                    Integer m1Val = v1.get(finalJ);
                                    ArrayList<Integer> v2 = m2.get(finalI);
                                    Integer m2Val = v2.get(finalJ);
                                    result.get(finalI).add(finalJ,  m1Val + m2Val);
                            }
                        }
                );
            }
        }
for(int i=0;i
数组的类型为
ArrayLists
,其中每个嵌套的
ArrayList
描述一列。它们的维度为
numRows
x
numCols
。我测量了此操作的时间,将一对随机生成的大小为10000 x 10000的矩阵求和,发现单线程版本需要123秒,多线程(6核intel i7上的11个线程)版本大约需要300秒

在这种情况下,我选择使用ArrayList,因为它们允许不安全的并发访问,即我可以同时修改ArrayList的不同部分。然而,这并没有提供我所期望的任何额外的加速。我想为什么我没有看到加速是因为以下原因:

  • 内存总线阻塞,无法处理线程对RAM的多次读/写操作,因此内存速度是一个瓶颈
  • 我使用Executors.newFixedThreadPool执行此操作。每次从RAM读取数据后,一级缓存都会更新,以提高数据访问速度。但是,此缓存无效,因为在给定处理器上的线程上执行的下一个任务可能需要内存中不同位置的数据,而这些数据可能不会缓存在L1或L2级别,从而增加了时间

  • 这些猜测有意义吗?我可能看不到任何其他解释?

    您有两个主要问题:

  • 您正在为作为矩阵加法的一部分执行的每个加法安排一个runnable。创建Runnable、将其放入线程安全队列(由线程池内部使用)以及让工作线程轮询该队列以执行任务,都会带来巨大的开销
  • 您对矩阵(
    ArrayLists
    )使用的数据结构非常低效,数据局部性差,访问单个项的开销很大
  • 1和2都会导致许多额外的CPU周期被完全浪费;它们还都会导致糟糕的数据局部性,导致超出必要的缓存未命中

    此外,由于使用非线程安全的数据结构(本例中为ArrayList,因为它允许不安全的并发访问)来收集结果,因此得到的结果不正确;如果没有为每个结果预先填充
    Integer
    值,那么当列表扩展并覆盖早期数据时,您将丢失数据

    一个有效的办法是:

  • 在线程池中放入尽可能多的CPU核心线程。给每个线程分配矩阵的一部分,并让每个
    Runnable
    对整个部分执行加法。这意味着,如果您有8个内核和8个工作线程,那么每个线程将处理一个Runnable,该Runnable将在矩阵的12.5%上执行加法
  • 对数据结构使用
    int[]]
    ,或者更好,使用
    int[]
    并对
    行*宽+列的索引进行自己的计算。这提供了更好的数据局部性,并且不进行任何自动装箱和取消装箱,从而提高了速度。使用
    int[]
    特别适合于添加矩阵,因为您可以将矩阵视为数组-您不需要了解行和列,只需
    result[i]=m1[i]+m2[i]

  • 这些猜测对我来说很有意义。所有并行都会创建上下文切换,因为您必须管理N个并行上下文并相应地进行切换,而且过度并行可能会造成人为瓶颈,资源可能不得不平衡多个消费者的需求,使其无法以合理的速率提供。我对Java并发并不完全熟悉,但是,您不是为矩阵中的每个元素都创建了一个独特的可运行的,这会导致显著的减速吗?我现在尝试了这个方法,使用ExecutorService(11个线程),每行10000个元素创建一个新任务,需要约15-20秒(有些解决方案需要约5-10秒,但包含并发修改的问题,我对调试不感兴趣)。使用并行流API大约需要25-30秒。我不确定我在这两种情况下都采取了最佳方法,但它明显优于您的结果。谢谢!你的建议明显加快了速度。对于每个正好包含10亿个整数的矩阵,单个线程花费的时间为7346毫秒,而12个线程花费的时间为1567毫秒。