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)
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编译器没有