如何使用Haskell中的策略编写并行约简?
在高性能计算中,求和、积等通常使用“并行缩减”进行计算,该缩减需要n个元素,并在O(logn)时间内完成(给定足够的并行性)。在Haskell中,我们通常使用折叠进行此类计算,但计算时间始终与列表长度成线性关系 Data Parallel Haskell有一些内置的功能,但是在列表的公共框架中呢?我们可以用如何使用Haskell中的策略编写并行约简?,haskell,parallel-processing,Haskell,Parallel Processing,在高性能计算中,求和、积等通常使用“并行缩减”进行计算,该缩减需要n个元素,并在O(logn)时间内完成(给定足够的并行性)。在Haskell中,我们通常使用折叠进行此类计算,但计算时间始终与列表长度成线性关系 Data Parallel Haskell有一些内置的功能,但是在列表的公共框架中呢?我们可以用控制、并行、策略来实现吗 那么,假设f是关联的,我们怎么写呢 parFold::(a->a->a)->[a]->a 因此,parFold f xs只需要长度为的时间对数xs?这似乎是一个好的开
控制、并行、策略来实现吗
那么,假设f
是关联的,我们怎么写呢
parFold::(a->a->a)->[a]->a
因此,parFold f xs
只需要长度为的时间对数xs
?这似乎是一个好的开始:
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倍以下。我认为列表不是适合这种情况的数据类型。因为它只是一个链表,所以必须按顺序访问数据。虽然您可以并行地评估项目,但在缩减步骤中您不会获得太多收益。如果你真的需要一个列表,我认为最好的功能就是
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)
我冒昧地假设您的数据是mempty的Monoid
,如果这不可能,您可以用自己的空类型替换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)
根据工作的具体分布方式,其中一个可能比其他的更有效
如果您可以使用对索引(数组、向量、映射等)有良好支持的数据结构,那么您可以对缩减步骤进行二进制细分,这可能总体上会更好。不确定您的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'
正如人们所注意到的,列表对于递归并行拆分来说是一个糟糕的数据结构。您需要某种二叉树/绳索结构,如Fortress语言:谢谢,John。我喜欢在块上使用foldl。但是,在每个块被缩减后,外部的foldl’是连续的,其输入可能非常大。表达递归的最佳方式是什么?输入可以是列表,也可以不是列表,但这应该可以使用策略来表示。reducedList
中的parMap
函数将并行计算所有块。但是,如果您的输入太大,不想一次将其全部加载到内存中,那么您可以使用laziness和parBuffer。我在parBuffer
方面取得了非常好的成功,因为它允许您利用并行性和惰性。我认为如果您使用reducedList=withStrategy(parbuffer10rseq),它将起作用。映射(foldl'f mempty)
。我认为这比列表的递归要好,因为可以避免多次遍历。