在高性能计算中,总和,产品等通常使用“并行缩减”计算,该“并行缩减”采用 n 元素并在O(log n )时间内完成(给予足够的并行性)。在Haskell中,我们通常使用 fold 进行此类计算,但评估时间在列表长度上始终是线性的。
Data Parallel Haskell内置了一些内容,但是在列表的通用框架中呢?我们可以使用Control.Parallel.Strategies
吗?
因此,假设f
是关联的,我们如何编写
parFold :: (a -> a -> a) -> [a] -> a
所以parFold f xs
只需要length xs
中的时间对数?
答案 0 :(得分:7)
我认为列表不是正确的数据类型。因为它只是一个链表,所以必须按顺序访问数据。虽然您可以并行评估项目,但在减少步骤中您将无法获得太多收益。如果你真的需要一个List,我认为最好的功能就是
parFold f = foldl1' f . withStrategy (parList rseq)
或者
parFold f = foldl1' f . withStrategy (parBuffer 5 rseq)
如果减少步骤很复杂,您可以通过细分列表来获得收益:
parReduce f = foldl' f mempty . reducedList . chunkList . withStrategy (parList rseq)
where
chunkList list = let (l,ls) = splitAt 1000 list in l : chunkList ls
reducedList = parMap rseq (foldl' f mempty)
我冒昧地假设你的数据是Monoid
的mempty,如果这是不可能的,你可以用你自己的空类型替换mempty,或者更糟糕的情况下使用foldl1'
。
此处使用Control.Parallel.Strategies
的两个运算符。 parList
并行评估列表中的所有项目。之后,chunkList
将列表分成1000个元素的块。然后,每个块都由parMap
并行减少。
您也可以尝试
parReduce2 f = foldl' f mempty . reducedList . chunkList
where
chunkList list = let (l,ls) = splitAt 1000 list in l : chunkList ls
reducedList = parMap rseq (foldl' f mempty)
根据工作的确切分配方式,其中一项可能比其他工作更有效。
如果您可以使用对索引具有良好支持的数据结构(数组,矢量,地图等),那么您可以为缩减步骤执行二进制细分,这可能会更好。
答案 1 :(得分:1)
这似乎是一个好的开始:
parFold :: (a -> a -> a) -> [a] -> a
parFold f = go
where
strategy = parList rseq
go [x] = x
go xs = go (reduce xs `using` strategy)
reduce (x:y:xs) = f x y : reduce xs
reduce list = list -- empty or singleton list
它有效,但并行性并不是那么好。用parList
之类的内容替换parListChunks 1000
会有所帮助,但在8核计算机上加速仍然限制在1.5倍以下。
答案 2 :(得分:1)
不确定您的parFold
函数应该做什么。如果这是一个foldr或foldl的并行版本,我认为它的定义是错误的。
parFold :: (a -> a -> a) -> [a] -> a
// fold right in haskell (takes 3 arguments)
foldr :: (a -> b -> b) -> b -> [a] -> b
Fold将相同的函数应用于列表的每个元素,并累积每个应用程序的结果。我想,提出它的并行版本将要求元素的函数应用程序并行完成 - 有点像parList
那样。
par_foldr :: (NFData a, NFData b) => (a -> b -> b) -> b -> [a] -> b
par_foldr f z [] = z
par_foldr f z (x:xs) = res `using` \ _ -> rseq x' `par` rdeepseq res
where x' = par_foldr f z xs
res = x `f` x'