多线程矩阵加法比java中的单线程版本花费的时间更长

时间:2021-03-29 02:48:45

标签: 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 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);
                            }
                        }
                );
            }
        }

数组是 ArrayLists<ArrayList<Integer>> 类型,其中每个嵌套的 ArrayList 描述一列。它们的尺寸为 numRows x numCols。我测量了这个操作的时间,将一对随机生成的大小为 10000 x 10000 的矩阵相加,发现单线程版本花了我 123 秒,多线程(6 核 intel i7 上的 11 个线程)版本花了我大约 300 秒。

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

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

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

1 个答案:

答案 0 :(得分:2)

您有两个主要问题:

  1. 您正在为作为矩阵加法的一部分执行的每一个加法安排一个可运行的程序。创建 Runnable、将其放入线程安全队列(由线程池内部使用)以及让工作线程轮询该队列以获取任务会产生巨大的开销。
  2. 您对矩阵 (ArrayLists<ArrayList<Integer>>) 使用了一种非常低效的数据结构,数据局部性很差,访问单个项目的开销很大。

1 和 2 都会导致许多额外的 CPU 周期完全被浪费掉;它们还会导致数据局部性差,导致缓存未命中数超过必要。

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

一种有效的方法是:

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