Haskell 平行;“折叠”;在哈斯克尔

Haskell 平行;“折叠”;在哈斯克尔,haskell,parallel-processing,Haskell,Parallel Processing,我有一个类型如下的函数: union :: a -> a -> a 而a具有可加性特性。因此,我们可以将联合视为(+) 比如说,我们有[a],并且希望执行并行“折叠”,对于非并行折叠,我们只能执行以下操作: foldl1' union [a] 但如何并行执行呢? 我可以演示Num值和(+)函数的问题 例如,我们有一个列表[1,2,3,4,5,6]和(+) 同时,我们应该分开 [1,2,3] (+) [4,5,6] [1,2] (+) [3] (+) [4,5] (+) [6] (

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

union :: a -> a -> a
a
具有可加性特性。因此,我们可以将
联合
视为
(+)

比如说,我们有
[a]
,并且希望执行并行
“折叠”
,对于非并行折叠,我们只能执行以下操作:

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
可加性,我们拆分列表或按任意顺序执行操作


使用任何标准库有什么方法可以做到这一点吗?

您需要将您的
联合
推广到任何关联二进制运算符⊕ 以致于⊕ (b)⊕ c==a⊕ (b)⊕ c) 。如果同时你有一个单位元素,它与⊕, 你有一个幺半群

关联性的重要方面是,您可以在列表中任意分组连续元素的块,并且⊕ 他们以任何顺序,因为⊕ (b)⊕ (c)⊕ d) )==(a⊕ (b)⊕ (c)⊕ d) -每个括号可以并行计算;然后,您需要“减少”所有括号中的“总和”,这样您就可以对map reduce进行排序

为了使这种并行化变得有意义,您需要块处理操作比⊕ - 否则,⊕ 顺序比分块好。一种情况是当你有一个随机访问的“列表”——比如说,一个数组。具有大量并行折叠功能

如果你想亲自实践一个,你需要选择一个好的复杂函数⊕ 这样的好处就会显现出来

例如:

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
总运行时间为8.78秒,而

a +RTS -N2 -s

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

您所描述的本质上是一个幺半群。在GHCI中:

Prelude> :m + Data.Monoid
Prelude Data.Monoid> :info Monoid
class Monoid a where
  mempty :: a
  mappend :: a -> a -> a
  mconcat :: [a] -> a
正如您所见,幺半群有三个相关函数:

  • mempty
    函数有点像幺半群的恒等式函数。例如,
    Num
    可以作为一个幺半群来处理两个操作:求和和和积。对于总和,
    mempty
    定义为
    0
    。对于产品,
    mempty
    定义为
    1

    mempty `mappend` a = a
    a `mappend` mempty = a
    
  • mappend
    函数与您的
    union
    函数类似。例如,对于
    Num
    s之和,将
    mappend
    定义为
    (+)
    ,对于
    Num
    s的乘积,将
    mappend
    定义为
    (*)

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

    (a `mappend` b) `mappend` c =  a `mappend` (b `mappend` c)
    
  • 但是请注意,Haskell并不强制执行幺半群法。因此,如果您将一个类型作为
    Monoid
    typeclass的实例,那么您有责任确保它满足Monoid法则

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

    给您一个如何使类型成为monoid typeclass实例的示例:

    newtype Sum a = Sum { getSum :: a }
    
    instance Num a => Monoid (Sum a) where
        mempty = 0
        mappend = (+)
    
    就这样。我们不需要定义
    mconcat
    函数,因为它有一个依赖于
    mempty
    mappend
    的默认实现。因此,当我们定义
    mempty
    mappend
    时,我们可以免费获得
    mconcat

    现在您可以按如下方式使用
    Sum

    getSum . mconcat $ map Sum [1..6]
    
    data Something a = Something { getSomething :: a }
    
    instance Monoid (Something a) where
        mempty  = unionEmpty
        mappend = union
        mconcat = foldParallel
    
    getSomething . mconcat $ map Something [1..6]
    
    这就是正在发生的事情:

  • 您正在将
    Sum
    构造函数映射到
    [1..6]
    上,以生成
    [Sum 1,Sum 2,Sum 3,Sum 4,Sum 5,Sum 6]
  • 您将结果列表提供给
    mconcat
    ,该列表将其折叠为
    Sum 21
  • 您可以使用
    getSum
    Sum 21
    中提取
    Num
  • 但是请注意,
    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
    的一个新实例,如下所示:

    getSum . mconcat $ map Sum [1..6]
    
    data Something a = Something { getSomething :: a }
    
    instance Monoid (Something a) where
        mempty  = unionEmpty
        mappend = union
        mconcat = foldParallel
    
    getSomething . mconcat $ map Something [1..6]
    
    我们使用它如下:

    getSum . mconcat $ map Sum [1..6]
    
    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
    。你可以根据自己的喜好来定义它。

    请看:我不清楚折叠平行的平行性是什么。使用关联性法则只是使能器。您需要确保拆分速度也比mappend快。拆分列表的额外开销必须通过并行执行折叠节省的时间来补偿。否则,在正常折叠上使用平行折叠将毫无意义。
    foldParallel
    函数本身并没有什么相似之处。然而,因为它将列表分为两部分,并递归地处理每个子列表,所以Haskell可以进行优化,并在不同的核心上处理每个子列表。因此,它支持并行性。这并不保证。AFAIK GHC永远不会“进行优化并在不同的核心上处理每个子列表”。并行性总是显式的。当然,并行项/图缩减是可能的,但在实践中有多少是可能的?如果某个Haskell编译器没有