在Haskell中并行“折叠”

时间:2013-10-01 13:47:53

标签: haskell parallel-processing

我有一个类型如下的函数:

union :: a -> a -> a

a具有可加性属性。因此,我们可以将union视为(+)

的版本

说,我们有[a],并希望执行并行"folding",对于非并行折叠,我们只能这样做:

foldl1' union [a]

但如何并行执行? 我可以在Num值和(+)函数上演示问题。

例如,我们有一个列表[1,2,3,4,5,6](+) 同时我们应该拆分

[1,2,3] (+) [4,5,6]
[1,2] (+) [3] (+) [4,5] (+) [6]
([1] (+) [2]) (+) ([3] (+) [4]) (+) ([5] (+) [6])

然后我们要并行执行每个(+)操作,并结合回答

[3] (+) [7] (+) [11] = 21

请注意,由于a可加性,我们会拆分列表或以任何顺序执行操作。

有没有办法使用任何标准库?

2 个答案:

答案 0 :(得分:12)

您需要将union推广到任何关联二元运算符⊕,使得(a⊕b)⊕c==a⊕(b⊕c)。如果同时你甚至有一个相对于neutral中性的单位元素,你就有一个幺半群。

关联性的一个重要方面是你可以在一个列表中任意分组连续元素的块,并且它们可以按任何顺序排列,因为⊕(b⊕(c⊕d))==(a⊕b)⊕( c⊕d) - 每个支架可以并行计算;那么你需要“减少”所有括号的“总和”,并且你已经对map-reduce进行了排序。

为了让这种并行化有意义,你需要使分块操作比⊕更快 - 否则,顺序执行is比分块更好。一个这样的情况是当你有一个随机访问“列表” - 比如一个数组。 Data.Array.Repa有很多平行折叠功能。

如果您正在考虑自己实施一个实践,那么您需要选择一个好的复杂功能,以便获得好处。

例如:

import Control.Parallel
import Data.List

pfold :: (Num a, Enum a) => (a -> a -> a) -> [a] -> a
pfold _ [x] = x
pfold mappend xs  = (ys `par` zs) `pseq` (ys `mappend` zs) where
  len = length xs
  (ys', zs') = splitAt (len `div` 2) xs
  ys = pfold mappend ys'
  zs = pfold mappend zs'

main = print $ pfold (+) [ foldl' (*) 1 [1..x] | x <- [1..5000] ]
  -- need a more complicated computation than (+) of numbers
  -- so we produce a list of products of many numbers

在这里,我故意使用一个仅在本地称为mappend的关联操作,以表明它可以用于比幺半群更弱的概念 - 只有关联性对并行性很重要;因为并行性只对非空列表有意义,所以不需要mempty

ghc -O2 -threaded a.hs
a +RTS -N1 -s

总运行时间为8.78秒,而

a +RTS -N2 -s

在我的双核笔记本电脑上总运行时间为5.89秒。显然,在这台机器上尝试超过-N2没有意义。

答案 1 :(得分:4)

你所描述的基本上是一个幺半群。在GHCI:

Prelude> :m + Data.Monoid
Prelude Data.Monoid> :info Monoid
class Monoid a where
  mempty :: a
  mappend :: a -> a -> a
  mconcat :: [a] -> a

正如你所看到的,monoid有三个相关的函数:

  1. mempty函数有点像幺半群的身份函数。例如,Num可以表现为monoid apropos两个操作:sum和product。总和mempty定义为0。对于产品mempty,定义为1

    mempty `mappend` a = a
    a `mappend` mempty = a
    
  2. mappend功能类似于您的union功能。例如,Num s mappend的总和定义为(+),而Num s mappend的产品定义为(*)

  3. mconcat功能类似于折叠。然而,由于幺半群的特性,无论我们是从左侧折叠,从右侧折叠还是从任意位置折叠都无关紧要。这是因为mappend应该是关联的:

    (a `mappend` b) `mappend` c =  a `mappend` (b `mappend` c)
    
  4. 但请注意,Haskell并未强制执行幺半群定律。因此,如果你创建一个类型Monoid类型类的实例,那么你有责任确保它满足幺半群定律。

    在您的情况下,很难理解union对其类型签名的行为:a -> a -> a。当然,你不能使类型变量成为类型类的实例。这是不允许的。你需要更具体。 union实际上做了什么?

    为您举例说明如何使类型成为monoid类型类的实例:

    newtype Sum a = Sum { getSum :: a }
    
    instance Num a => Monoid (Sum a) where
        mempty = 0
        mappend = (+)
    

    就是这样。我们不需要定义mconcat函数,因为它具有依赖于memptymappend的默认实现。因此,当我们定义memptymappend时,我们会免费获得mconcat

    现在您可以使用Sum,如下所示:

    getSum . mconcat $ map Sum [1..6]
    

    这就是发生的事情:

    1. 您将Sum构造函数映射到[1..6]以生成[Sum 1, Sum 2, Sum 3, Sum 4, Sum 5, Sum 6]
    2. 您将结果列表提供给mconcat,将其折叠为Sum 21
    3. 您使用getSumNum中提取Sum 21
    4. 但请注意,mconcat的默认实现是foldr mappend mempty(即它是正确的折叠)。对于大多数情况,默认实现就足够了。但是在您的情况下,您可能希望覆盖默认实现:

      foldParallel :: Monoid a => [a] -> a
      foldParallel []  = mempty
      foldParallel [a] = a
      foldParallel xs  = foldParallel left `mappend` foldParallel right
          where size = length xs
                index = (size + size `mod` 2) `div` 2
                (left, right) = splitAt index xs
      

      现在我们可以创建Monoid的新实例,如下所示:

      data Something a = Something { getSomething :: a }
      
      instance Monoid (Something a) where
          mempty  = unionEmpty
          mappend = union
          mconcat = foldParallel
      

      我们按如下方式使用它:

      getSomething . mconcat $ map Something [1..6]
      

      请注意,我将mempty定义为unionEmpty。我不知道union函数的作用类型是什么。因此,我不知道应该将mempty定义为什么。因此我只是称它为unionEmpty。根据您的需要定义它。