【问题标题】:Java parallel execution slower than sequentalJava 并行执行比顺序执行慢
【发布时间】:2016-01-13 02:58:21
【问题描述】:

我现在正在使用 Java 中的并行执行。昨天我尝试测量执行时间并得到一些不清楚的结果。

任务:使用并行模式和顺序求和数组。这是我的并行代码:

public static int sumArrayParallel(int[] numbers) throws ExecutionException, InterruptedException {
    int cpus = Runtime.getRuntime().availableProcessors();
    ExecutorService service = Executors.newFixedThreadPool(cpus);
    List<FutureTask<Integer>> tasks = new ArrayList<>();
    int blockSize = (numbers.length + cpus - 1) / cpus;

    for (int i = 0; i < numbers.length; i++) {
        final int start = blockSize * i;
        final int end = Math.min(blockSize * ( i + 1 ), numbers.length);
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
            public Integer call() {
                int sub = 0;
                for (int j = start; j < end; j++)
                    sub += numbers[j];
                return sub;
            }
        });
        tasks.add(futureTask);
        service.execute(futureTask);
    }
    int sum = 0;
    for(Future<Integer> task: tasks)
        sum += task.get();
    service.shutdown();        
    return  sum;
}

对于顺序来说非常简单:

  public static int sumArraySequential(int[] arr) {
    int sum = 0;
    for( int num : arr ) {
        sum += num;
    }
    return  sum;
};

所以,顺序函数的工作速度比并行函数快 2-4 倍。我做错了什么?

【问题讨论】:

  • 为什么你认为并行总是更快?也发布您系统的硬件/CPU 详细信息。
  • 输入大小是多少?
  • 也许,并行代码中的所有这些初始设置比执行这个简单循环的成本要高得多,它可以在 CPU 上以极快的速度执行。
  • 最终的 int[] 数字,对吧?
  • @Leo 在 Java 8 中你不必explicitly declare final

标签: java parallel-processing


【解决方案1】:

求和,处理明智,是一个真的简单的任务。加法是 一个 CPU 周期。

从内存中取出数据是一项非常昂贵的任务。根据您的数组大小,它可能存在于主内存中,而不是任何 L1、L2、L3 缓存中。从主内存中取出数据需要 数百 个 CPU 周期。

现在,当您在单个线程上按顺序进行求和时,CPU 会假设您需要更多正在处理的部分的内存,并将其抢先加载到 L1/L2/L3 缓存中。这种优化基本上完全取消了从主存获取数据的“数百个 CPU 周期”,因为数据在你想要求和的时候已经在缓存中了。

当您现在尝试并行化任务时,您将数组拆分为多个块。优化器不知道将哪些部分加载到缓存中,因为它们可能会乱序执行。对于并行任务,缓存中可能已经没有数据,导致必须等待数百个 CPU 周期才能从主内存中获取数据。

因此,最终,您的任务不受 CPU 可以执行的 处理 量(通过并行化增加)的限制,而是从内存中获取数据的数量和速度(即更容易在单个顺序程序中优化)。这可能解释了您的“意外”结果。

此外,根据您的输入大小,线程的初始化比处理需要更多的时间,但我只能假设您使用的是大数组大小,所以这并不重要。

【讨论】:

    【解决方案2】:

    你的代码不对。

    您正在创建 N 个 elements 任务,而您应该创建 M 个 blocks 任务。 :-)

    修复你的主循环

    for (int i = 0; i < numbers.length; i++) {
    

    迭代块,而不是元素。

    ps。如果你稍微改变一下你的代码,你就会清楚地看到发生了什么

        int sum = 0;
        for(Future<Integer> task: tasks) {
            sum += task.get();
            System.out.println(sum);
        }
    

    【讨论】:

      【解决方案3】:

      在顺序版本中,您只使用原语,这本身就很快。

      在并行或并发版本中,您创建了许多对象,这会在创建和使用中产生开销。

      你没有说你测试这个的数组大小。我猜想对于较大的 numbers.length 值,性能会相对更好。

      【讨论】:

        【解决方案4】:

        首先,正如 Leo 所说,您必须修复循环,以免创建 numbers.length 线程。

        其次,正如其他人所说,您的顺序解决方案可能会更快,因为您的输入大小以及您可能还测量任务创建。

        为了获得更好的测量结果,我建议您:

        • 确保您使用的计算机有足够的内核来实际执行更快的并行运行
        • 使用大型输入数组(至少数百万个元素)。
        • 参加https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/CyclicBarrier.html 这是一个为您提供等待线程“相遇”甚至稍后“集合”的选项的类。假设您同意在 9:00 与您的 5 个朋友见面。然后你等待你的朋友,然后你们可能会在 9:00 或者可能在 9:05 集合,无论如何你都在等待。然后你可能会同意做一些单独的事情,然后在 11:00 再次见面,以此类推。这对您很有用,因为您可以在以下位置设置障碍:

        创建一个 CyclicBarrier,并将 barrier.await() 作为调用方法的第一条语句。然后在您的 main 方法中,您还调用 barrier.await() 并在每个线程到达障碍后立即开始您的基准测试。 这样您就不必测量线程创建和启动性能,尽管这实际上可能与您有关!这取决于您的问题的语义。

        【讨论】: