我正在使用并行的Haskell函数par
和pseq
,我发现了一些有趣的东西。
我的示例基于Real World Haskell的书(Parallel programming in Haskell)中的示例:
常用代码:
import Control.Parallel (par, pseq)
-- <<sorting code goes here>>
force :: [a] -> ()
force xs = go xs `pseq` ()
where go (_:xs) = go xs
go [] = 1
main = do
print $ take 10 $ parSort [0..1000000]
排序代码1(取自本书):
parSort :: (Ord a) => [a] -> [a]
parSort (x:xs) = force greater `par` (force lesser `pseq`
(lesser ++ x:greater))
where lesser = parSort [y | y <- xs, y < x]
greater = parSort [y | y <- xs, y >= x]
parSort _ = []
排序代码2(我的自定义变体):
parSort :: (Ord a) => [a] -> [a]
parSort (x:xs) = force greater `par` (lesser ++ x:greater)
where lesser = parSort [y | y <- xs, y < x]
greater = parSort [y | y <- xs, y >= x]
parSort _ = []
编译&amp;运行:ghc -O2 -threaded --make Main.hs && time ./Main +RTS -N8
有趣的是,我的变体比书籍快一点:
sorting code 1 - avg. 16 seconds
sorting code 2 - avg. 14 seconds
我想问你为什么我们可以观察到这种行为,以及这本书的解决方案是否会给我带来任何好处。我很想深深理解为什么这个解决方案可以表现更好。
答案 0 :(得分:7)
我会说这是因为您的自定义变体不会强制列表的第一部分。让我们来看看顶层发生的事情:你强制列表的右半部分,但不强制左侧部分。当你打印前10个元素时,你只会懒得评估左边部分的前10个元素,而剩下的部分则没有评估。
另一方面,本书的解决方案强制要求两个部分,所以在打印前10个元素之前,你要评估左边和右边的部分。
不要打印前10个元素,而是尝试打印最后一个元素,如
print $ last $ parSort data
然后算法的两个变体都必须评估整个列表。或者在排序之后和打印之前强制整个列表。
注意使用此算法对[0..100000]
进行排序效率非常低,因为您总是选择最差的枢轴,因此需要 O(n ^ 2)时间。测量结果根本不会给出有意义的结果。如果您希望使用 O(n log n)时间获得不错的结果,请使用类似随机的数据提供算法。您可以找到一种简单的方法来创建随机排列here。
注意:我建议您使用criterion来衡量代码,而不是使用time
。然后,您可以仅测量代码的相关部分,不包括初始化等,并强制输入和输出数据,以便精确测量您感兴趣的部分。