Java 8中新引入的Arrays.parallelPrefix(...)如何工作?

时间:2018-10-24 06:04:17

标签: java arrays java-8

我遇到了Java 8中引入的Arrays.parallelPrefix

此重载方法以累积方式对输入数组的每个元素执行操作。例如来自文档:

  

并行累积给定数组中每个元素的位置,   使用提供的功能。例如,如果数组最初成立   [2,1,0,3]并执行加法运算,然后返回   数组保存[2、3、3、6]。并行前缀计算通常更多   对于大型数组,它比顺序循环更有效。

那么,当一个术语的运算依赖于前一个术语的运算结果等等时,Java如何在parallel中完成此任务?

我尝试自己检查代码,他们确实使用ForkJoinTasks,但是如何合并结果以获取最终数组并不是那么简单。

3 个答案:

答案 0 :(得分:26)

要点是运算符是一个

  

无副作用,关联功能

这意味着

(a op b) op c == a op (b op c)

因此,如果您将数组分为两半,并在每一半上递归应用parallelPrefix方法,则可以稍后通过对数组的后半部分的每个元素应用运算符来合并部分结果。上半部的最后一个元素。

考虑带有附加示例的[2, 1, 0, 3]。如果将数组分成两半并在每一半上执行操作,则会得到:

[2, 3]    and    [0, 3]

然后,为了合并它们,请将3(上半部分的最后一个元素)添加到下半部分的每个元素,并得到:

[2, 3, 3, 6]

编辑:此答案提出了一种并行计算数组前缀的方法。它不一定是最有效的方法,也不一定是JDK实现所使用的方法。您可以进一步了解用于解决问题here的并行算法。

答案 1 :(得分:18)

Eran’s answer中所述,此操作利用了函数的关联性。

然后,有两个基本步骤。第一个是实际的前缀操作(就评估而言,需要前面的一个或多个元素),它并行应用于数组的各个部分。每个部分运算的结果(与最后一个元素相同)是剩余数组的偏移量。

例如对于以下数组,使用sum作为前缀运算和四个处理器

  4    9    5    1    0    5    1    6    6    4    6    5    1    6    9    3  

我们得到

  4 → 13 → 18 → 19    0 →  5 →  6 → 12    6 → 10 → 16 → 21    1 →  7 → 16 → 19  
                 ↓                   ↓                   ↓                   ↓  
                19                  12                  21                  19  

现在,我们利用关联性将前缀运算首先应用于偏移量

                 ↓                   ↓                   ↓                   ↓  
                19         →        31         →        52         →        71  

然后,我们进入第二阶段,将这些偏移量应用于下一个块的每个元素,这是一个完全可并行化的操作,因为不再依赖于先前的元素

                     19   19   19   19   31   31   31   31   52   52   52   52  
                      ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓  
  4   13   18   19   19   24   25   31   37   41   47   52   53   59   68   71  

当我们对八个线程使用相同的示例时,

  4    9    5    1    0    5    1    6    6    4    6    5    1    6    9    3  

  4 → 13    5 →  6    0 →  5    1 →  7    6 → 10    6 → 11    1 →  7    9 → 12  
       ↓         ↓         ↓         ↓         ↓         ↓         ↓         ↓  
      13         6         5         7        10        11         7        12  

       ↓         ↓         ↓         ↓         ↓         ↓         ↓         ↓  
      13    →   19    →   24    →   31    →   41    →   52    →   59    →   71  

           13   13   19   19   24   24   31   31   41   41   52   52   59   59  
            ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓  
  4   13   18   19   19   24   25   31   37   41   47   52   53   59   68   71  

我们看到了明显的好处,即使我们使用更简单的策略将两个工作块都保持相同,换句话说,在第二阶段接受一个空闲的工作线程。第一阶段需要大约aboutn,第二阶段需要⅛n,总共需要¼n的运算(其中 n 是整个数组的顺序前缀评估的成本)。当然,只有在最佳情况下才能做到。

相反,当我们只有两个处理器时

  4    9    5    1    0    5    1    6    6    4    6    5    1    6    9    3  


  4 → 13 → 18 → 19 → 19 → 24 → 25 → 31    6 → 10 → 16 → 21 → 22 → 28 → 37 → 40  
                                     ↓                                       ↓  
                                    31                                      40  

                                     ↓                                       ↓  
                                    31                   →                  71  

                                         31   31   31   31   31   31   31   31  
                                          ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓  
  4   13   18   19   19   24   25   31   37   41   47   52   53   59   68   71  

只有重新分配第二阶段的工作,我们才能受益。如前所述,这是有可能的,因为第二阶段的工作不再存在元素之间的依赖关系。因此,我们可以任意拆分此操作,尽管它会使实现复杂化并可能带来额外的开销。

当我们在两个处理器之间分配第二阶段的工作时,第一阶段大约需要½n,第二阶段大约需要¼n,总共产生¾n,如果阵列足够大,这仍然是一个好处。

作为附加说明,您可能会注意到在准备第二阶段时计算出的偏移量与块中最后一个元素的结果相同。因此,只需分配该值,就可以将所需的操作数量每块减少一个。但是典型的情况是只有几个带有大量元素的块(随处理器数量扩展),因此每个块保存一个操作是无关紧要的。

答案 2 :(得分:1)

我同时阅读了两个答案,但仍然无法完全理解如何完成此操作,因此决定画一个例子。这是我想出的,假设这是我们开始的数组(带有3个CPU):

7, 9, 6, 1, 8, 7, 3, 4, 9

因此3个线程中的每个线程都将获得其工作块:

Thread 1:  7, 9, 6
Thread 2:  1, 8, 7
Thread 3:  3, 4, 9

由于文档要求使用 associative 函数,因此我们可以在第一个线程中计算总和,并在一个线程中计算部分和,当第一个已知时-所有这些都将计算出来。让我们看看7, 9, 6会变成什么:

7, 9, 6  -> 7, 16, 22

因此,第一个线程的总和为22-但是其他线程对此还一无所知,因此它们所做的是与之相反的工作,例如x。因此,线程2将为:

1, 8, 7 -> 1 (+x), 9 (+x), 16(+x) 

因此,第二个线程的总和为x + 16,因此在Thread 3中,我们将有:

3, 4, 9 -> 3 (+ x + 16), 7 (+ x + 16), 16 (+ x + 16)

3, 4, 9 -> x + 19, x + 23, x + 32

这样,我一知道x,就知道其他所有结果。

免责声明:我不确定这是如何实现的(我尝试查看代码-但这太复杂了。)