“reduce”函数可以在函数式编程中并行化吗?

Posted

技术标签:

【中文标题】“reduce”函数可以在函数式编程中并行化吗?【英文标题】:Could the "reduce" function be parallelized in Functional Programming? 【发布时间】:2016-02-06 22:09:08 【问题描述】:

函数式编程中,map 函数的一个好处是它可以实现为并行执行。

因此,在 4 核硬件上,此代码和map 的并行实现将允许同时处理 4 个值

let numbers = [0,1,2,3]
let increasedNumbers = numbers.map  $0 + 1 

好的,现在让我们谈谈reduce 函数。

返回重复调用 combine 的结果和一个累积的值 依次初始化为 initial 和 self 的每个元素的值,即 return combine(combine(...combine(combine(initial, self[0])), self[1]),...self[count-2]), self[count-1]).

我的问题:可以实现reduce 函数以便并行执行吗? 或者,根据定义,它是只能按顺序执行

例子:

let sum = numbers.reduce(0)  $0 + $1 

【问题讨论】:

这取决于操作...如果你有一个关联的,那么是的,你可以做 (a % b) % (c % d) 而不是 ((a%b)%c)%d 例如(并且在外部之前并行执行内部操作(如果%是操作) 有时您可以分解操作本身 - 一个很好的示例是排序,您可以执行诸如合并排序(可以并行)之类的操作,而不是插入排序;) 您可以认为mapreduce 的一个特例。现在这对您的问题意味着什么? :-) @Bergi:嗨,Bergi,我很难将map 视为reduce 的特例,因为map 确实由其他元素独立处理数组的每个元素。事实上,map 中的闭包仅接收数组的 i-th 元素。所以在这里我认为并行化没有问题。另一方面,reduce 中的闭包,当应用于第 i 个时,确实需要将reduce 应用于(i-1)-th 元素(这可能是并行化的问题)+ 当前元素的结果。跨度> @appzYourLife:所以你正在处理数组,而不是列表?并有热切的评价?理论上,reduce 可以产生一个结果,其部分可以并行评估。这在实践中是如何实现的,以及它以何种方式等同于您想到的 map 调用的并行评估,是另一回事。 【参考方案1】:

最常见的减少之一是所有元素的总和。

((a+b) + c) + d == (a + b) + (c+d)  # associative
a+b == b+a                          # commutative

这种相等性适用于整数,因此您可以将操作顺序从一个长依赖链更改为多个较短的依赖链,从而实现多线程和 SIMD 并行。

数学实数也是如此,but not for floating point numbers。在许多情况下,catastrophic cancellation 是意料之外的,因此最终结果将足够接近,值得获得巨大的性能提升。对于 C/C++ 编译器,这是 -ffast-math 选项启用的优化之一。 (对于 -ffast-math 的这一部分,有一个 -fassociative-math 选项,没有关于缺乏无穷大和 NaN 的假设。)

如果一个广泛的负载无法获取多个有用的值,则很难获得很大的 SIMD 加速。英特尔的 AVX2 增加了“聚集”负载,但开销非常高。使用 Haswell,仅使用标量代码通常更快,但后来的微架构确实具有更快的收集速度。因此,SIMD 减少对数组或其他连续存储的数据更有效

现代 SIMD 硬件通过将 2 个连续双精度浮点数加载到向量寄存器中来工作(例如,使用 16B 向量,如 x86 的 sse)。有一个packed-FP-add 指令将两个向量的对应元素相加。所谓的“垂直”向量操作(在两个向量中的对应元素之间发生相同的操作)比“水平”操作(将一个向量中的两个doubles 彼此相加)便宜得多。


因此,在 asm 级别,您有一个循环,它将所有偶数元素加到向量累加器的一半中,并将所有奇数元素加到另一半中。然后最后的一个水平操作将它们组合起来。因此,即使没有多线程,使用 SIMD 也需要关联操作(或者至少足够接近关联,就像浮点通常那样)。如果您的输入中有一个近似模式,例如 +1.001、-0.999,则将一个大正数添加到一个大负数所产生的取消错误可能比每次取消都单独发生时要严重得多。

对于更宽的向量或更窄的元素,向量累加器将容纳更多的元素,从而增加 SIMD 的优势。

现代硬件具有流水线执行单元,每个时钟可以支持一个(或有时两个)FP 向量加法,但每个周期的结果还没有准备好 5 个周期。饱和硬件的吞吐能力需要在循环中使用多个累加器,因此有 5 或 10 个独立的循环承载依赖链。 (具体而言,英特尔 Skylake 执行向量 FP 乘法、加法或 FMA(融合乘法加法),延迟为 4c,吞吐量为每 0.5c 一次。4c/0.5c = 8 次飞行中的 FP 加法一次使 Skylake 的 FP 数学饱和单位。每个操作可以是 8 个单精度浮点数、4 个双精度浮点数、16B 向量或标量的 32B 向量。(保持多个操作在运行中也可以加快标量的东西,但如果有任何数据-可用级别并行性,您可能可以对其进行矢量化以及使用多个累加器。)请参阅http://agner.org/optimize/ 了解 x86 指令时序、流水线描述和 asm 优化内容。但请注意,此处的所有内容都适用于带有 NEON、PPC Altivec 的 ARM ,以及其他 SIMD 架构。它们都有向量寄存器和类似的向量指令。

举个具体的例子,here's how gcc 5.3 auto-vectorizes a FP sum reduction。它只使用一个累加器,因此它错过了 Skylake 的 8 倍吞吐量。 clang 更聪明一点,uses two accumulators, but not as many as the loop unroll factor 获得了 Skylake 最大吞吐量的 1/4。请注意,如果您从编译选项中取出-ffast-math,则FP 循环使用addss(添加标量单)而不是addps(添加打包单)。整数循环仍然自动向量化,因为整数数学是关联的。

在实践中,内存带宽在大多数情况下是限制因素。 Haswell 和更高版本的 Intel CPU 可以从 L1 高速缓存每个周期承受两个 32B 负载。 In theory, they could sustain that from L2 cache。共享 L3 缓存是另一回事:它比主存快得多,但其带宽由所有内核共享。这使得 L1 或 L2 的缓存阻塞(又名loop tiling)在处理超过 256k 的数据时可以廉价地完成一个非常重要的优化。与其产生然后减少 10MiB 的数据,不如产生 128k 块并在它们仍在 L2 缓存中时减少它们,而不是生产者必须将它们推送到主内存而减速器必须将它们带回。高级语言,您最好的选择可能是希望实现为您执行此操作。不过,就 CPU 的实际工作而言,这是您理想中希望发生的事情。

请注意,所有 SIMD 加速内容都适用于在连续内存块上运行的单个线程中。 您(或您的函数式语言的编译器!)可以而且应该使用这两种技术,让多个线程分别使它们运行的​​内核上的执行单元饱和。


抱歉,此答案中缺少函数式编程。你可能已经猜到我是因为 SIMD 标签才看到这个问题的。 :P

我不会尝试从其他操作中进行概括。 IDK 你们函数式编程的人会通过减少来完成什么样的事情,但是加法或比较(查找最小值/最大值,计数匹配)是用作 SIMD 优化示例的那些。

【讨论】:

非常感谢您的出色回答。这东西非常有趣。 @appzYourLife:干杯。 :) 我什至不确定这种答案是你所希望的。 感谢您的更新和通知(实际上我没有注意到更新)。我会仔细阅读的。【参考方案2】:

有一些函数式编程语言的编译器可以并行化reducemap 函数。这是来自Futhark 编程语言的示例,它编译成并行的CUDA 和OpenCL 源代码:

let main (x: []i32) (y: []i32): i32 =
  reduce (+) 0 (map2 (*) x y)

也许可以编写一个编译器,将 Haskell 的一个子集翻译成 Futhark,尽管这还没有完成。 Futhark 语言不允许递归函数,但它们可能会在该语言的未来版本中实现。

【讨论】:

以上是关于“reduce”函数可以在函数式编程中并行化吗?的主要内容,如果未能解决你的问题,请参考以下文章

python函数式编程

MapReduce分布编程模型之函数式编程范式

函数式编程进阶:应用函子

Python函数式编程——map()reduce()

Python函数式编程,map/reduce,filter和sorted

函数式编程 & Python中的高阶函数map reduce filter 和sorted