【问题标题】:Addition of Integer 2D array elements using multi-threading in java slower than sequential addition在java中使用多线程添加整数2D数组元素比顺序添加慢
【发布时间】:2025-11-29 17:00:01
【问题描述】:

所以,我在 java 中练习多线程,并尝试按顺序和使用 4 个线程添加随机生成的 2D 整数数组的元素。我测量了我的代码的性能,出于某种原因,顺序部分比多线程快得多。下面是顺序加法的代码:

public class ArraySum2DNonMT {

private int[][] arrayToSum;
private int totalSum;

public ArraySum2DNonMT(int[][] arr){
    this.arrayToSum = arr;
    this.setTotalSum(0);
}

public void runSequential(){
    for(int i = 0; i < arrayToSum[0].length; i++){
        for(int j = 0; j < arrayToSum.length; j++){
            setTotalSum(getTotalSum() + arrayToSum[j][i]);
        }
    }
}

public int getTotalSum() {
    return totalSum;
}

public void setTotalSum(int totalSum) {
    this.totalSum = totalSum;
}

}

这里是多线程版本的代码:

package multiThreaded;

/**
* 
* @author Sahil Gupta
* 
* This class takes in a 2D integer array and adds it's contents. This
* addition will be concurrent between several threads which will divide
* the work of the array based on the threadID assigned to thread by the
* programmer. Assume that the passed in 2D array to the constructor is a 
* matrix with each array in the main array having same length.
*/

public class ArraySum2D implements Runnable{

private int[][] arrayToSum;
private int threadID;
private int totalSum;

public ArraySum2D(int[][] arr, int threadID){
    this.arrayToSum = arr;
    this.threadID = threadID;
    this.setTotalSum(0);
}

@Override
public void run() {
    int arrayCol = arrayToSum[0].length;
    int arrayRow = arrayToSum.length;
    int colStart = (int)((threadID%2) * (arrayCol/2));
    int rowStart = (int)((int)(threadID/2) * (arrayRow/2));
    int colEnd = colStart + (int)(arrayCol/2);
    int rowEnd = rowStart + (int)(arrayRow/2);

    for(int i = colStart; i < colEnd; i++){
        for(int j = rowStart; j < rowEnd; j++){
            setTotalSum(getTotalSum() + arrayToSum[j][i]);
        }
    }
}

public int getTotalSum() {
    return totalSum;
}

public void setTotalSum(int totalSum) {
    this.totalSum = totalSum;
}

}

这里是主要的:

package controller;

import java.util.Random;

import multiThreaded.ArraySum2D;
import sequentialNonMT.ArraySum2DNonMT;

public class ControllerMain {

private final static int cols = 20;
private final static int rows = 10;
private static volatile int[][] arrayToAdd = new int[rows][cols];
private static Random rand = new Random();
private static ArraySum2D a0, a1, a2, a3;

public static void main(String[] args) throws InterruptedException{

    for(int j = 0; j < rows; j++){
        for(int i = 0; i < cols; i++){
            arrayToAdd[j][i] = rand.nextInt(100);
        }
    }

    ArraySum2DNonMT a = new ArraySum2DNonMT(arrayToAdd);

    long startTimeSequential = System.nanoTime();
    a.runSequential();
    long estimatedTimeSequential = System.nanoTime() - startTimeSequential;

    System.out.println("The total sum calculated by sequential program is: " + a.getTotalSum());
    System.out.println("The total time taken by sequential program is: " + estimatedTimeSequential);

    a0 = new ArraySum2D(arrayToAdd, 0);
    a1 = new ArraySum2D(arrayToAdd, 1);
    a2 = new ArraySum2D(arrayToAdd, 2);
    a3 = new ArraySum2D(arrayToAdd, 3);
    Thread t0 = new Thread(a0);
    Thread t1 = new Thread(a1);
    Thread t2 = new Thread(a2);
    Thread t3 = new Thread(a3);

    long startTimeMultiThreaded = System.nanoTime();
    t0.start();
    t1.start();
    t2.start();
    t3.start();

    t0.join();
    t1.join();
    t2.join();
    t3.join();
    int Sum = addThreadSum();
    long estimatedTimeMultiThreaded = System.nanoTime() - startTimeMultiThreaded;

    System.out.println("The total sum calculated by multi threaded program is: " + Sum);
    System.out.println("The total time taken by multi threaded program is: " + estimatedTimeMultiThreaded);
}

private static int addThreadSum(){
    return a0.getTotalSum() + a1.getTotalSum() + a2.getTotalSum() + a3.getTotalSum();
}

}

我目前得到的输出显示了运行时的显着差异(此处以纳秒为单位测量)。这是我得到的:

The total sum calculated by sequential program is: 10109 
The total time taken by sequential program is: 46000
The total sum calculated by multi threaded program is: 10109
The total time taken by multi threaded program is: 641000

顺序代码大约快 13 倍。你能帮我指出我可能做错了什么吗?我有一个双核 i7 haswell,macbook air。我不确定为什么需要更长的时间,但我想到了一些可能导致此问题的想法:错误共享、过多的并行/线程(双核为 4 个)、缓存一致性协议可能对我不利,我缺少/不知道的其他一些基本多线程的东西。

请帮助我确定使多线程运行比顺序运行更快的具体原因和方法。非常感谢您帮助我!

编辑:有关处理器及其缓存的更多信息: 处理器名称:英特尔酷睿 i7 处理器速度:1.7 GHz 处理器数量:1 核心总数:2 L2 缓存(每核):256 KB 三级缓存:4 MB

根据英特尔的数据表,我认为它最多可以有 4 个线程。

附:这是我第一篇提问的帖子,但我一直在使用这个网站来消除疑惑。请原谅我犯的任何错误。

【问题讨论】:

  • 除非我是个智障,否则你将把每个线程花费的时间加起来......并结合起来。将它们加在一起可以得到比实际时间长近 4 倍的值,因为它们同时/并行运行,而不是一个接一个地运行。
  • 这似乎是一个非常小的基准数据集。由于实际计算时间如此之少,因此线程中的额外设置代码可能会有所不同。尝试大量数据,看看结果是否不同。
  • 嗨@BrantUnger,我不同意你所说的。我计算时间的方法是获取代码开始执行之前的系统时间和代码停止执行时的系统时间。我认为这是应该这样做的。当我尝试您所说的时,所花费的时间是相同的。但感谢您的意见!
  • @JamesMontagne 谢谢!我同意。它确实缩短了时间,并通过数百万个元素的输入大小给了我预期的结果。尽管程序没有像多线程预期的那样运行四倍,但它的运行速度确实快了 2-2.5 倍。还有其他建议吗?

标签: java arrays multithreading performance


【解决方案1】:

建立线程时有sizable amount of overhead。也就是说,如果您的示例数据集太小,则启动和拆除线程所花费的时间将大于代码的实际运行时间性能。

让我们主观地看待它。您有一个仅包含 200 个元素的数组。您的方法的运行时间为 O(nm),其中 n 是行大小,m 是列大小。

坦率地说,我唯一希望不会以这种方式快速处理 200 个元素的机器是我的旧 Pentium III 机器。即使那样,它也不会那么遥远。

我有一个相对强大的 i7-4770K,它可以做 4 个内核,每个内核有两个线程。如果我用这些较低的数字运行你的程序,我会得到大致相同的结果。

但是...如果我把我的界限设大一点呢?令n = 2**m*,令n = 9000。

不要关注总和。整数溢出完全破坏了我们从中获得的任何价值。

The total sum calculated by sequential program is: -570429863
The total time taken by sequential program is: 3369190200
The total sum calculated by multi threaded program is: -570429863
The total time taken by multi threaded program is: 934624554

线程版本的运行时间为 27%,或大约快 3.6 倍。或者用外行的话来说,3.36 秒对 934 毫秒。这是巨大的

线程并没有改变算法的性能——它在 O(nm) 时仍然非常低效——但它确实改变了运行时常数,所以它并不完全,但接近 1/4 时间。我能够从中获得优势的唯一原因是我推送的数据量很大。否则,线程是不值得的。

【讨论】:

  • 感谢@Makoto,这绝对有帮助。我想不出任何比 O(nm) 时间更快的方法,因为我们已经将它们全部加起来。如果您认为我们可以更快地做到这一点,请告诉我。
【解决方案2】:

我完全同意 Makoto,至于 为什么 你会看到多线程程序变慢:线程创建开销使如此小的数组的微小计算时间相形见绌,正如你可以通过提高数组大小。

虽然这不能直接回答您的问题,但我想您可能会觉得这很有趣,因为有一个微不足道的更改可以使 两个 版本更快。

考虑您的原始代码:

 for(int i = 0; i < arrayToSum[0].length; i++){
        for(int j = 0; j < arrayToSum.length; j++){
            setTotalSum(getTotalSum() + arrayToSum[j][i]);
        }
 }

在java中,真的没有二维数组这样的东西。相反,二维数组实际上是一个数组引用数组。 (很好的参考:http://www.willamette.edu/~gorr/classes/cs231/lectures/chapter9/arrays2d.htm)在您原来的 for 循环中,内部 (j) 循环遍历所有数组,一次获取一个元素。 (即添加所有数组的第一个元素,然后添加第二个元素,等等。)大型数组上的这种行为几乎可以保证您无法从内存缓存中获得帮助,因为您的代码具有非常糟糕的locality of reference

如果你交换迭代的顺序,像这样:

 for(int j = 0; j < arrayToSum.length; j++){ // <-- this used to be the inner loop
     for(int i = 0; i < arrayToSum[0].length; i++){
         setTotalSum(getTotalSum() + arrayToSum[j][i]);
     }
 }

现在您的内部循环一次按顺序遍历一个数组,并且您具有出色的引用局部性,这对缓存非常友好。

在我的机器上,第二个版本的运行速度几乎是第一个版本的 20 倍。 (而且,如果您有兴趣,在我的机器上,多线程版本的运行速度大约是单线程版本的 2.8 倍,因此当您结合这两个更改时,缓存友好,多线程数组求和运算的运行速度几乎是原始单线程缓存敌对 :) 版本的 60 倍

【讨论】:

  • 感谢@JVMATL!这是一个很好的收获。我知道这种技术和它的缓存友好性,但是,在这里完全错过了它。它确实将我的性能提高了至少 20 倍。