注意:本文已于2011-06-10进行了完全重写;感谢Peter的帮助。此外,请不要因为我没有接受一个答案而感到不满,因为这个问题似乎是比较开放的。(但是,如果你解决了它,当然会得到勾选标记)。
另一个用户发布了一个关于并行化归并排序的问题。我想写一个简单的解决方案,但不幸的是,它并没有比顺序版本快多少。
问题陈述
归并排序是一种分治算法,其中计算的叶子可以并行化。
代码的工作方式如下:将列表转换为表示计算节点的树。然后,合并步骤返回每个节点的列表。理论上,我们应该看到一些显著的性能提升,因为我们从一个O(n log n)算法转换为具有无限处理器的O(n)算法。
当参数l(级别)小于零时,计算的前几步是并行化的。这是通过[通过变量strat]选择rpar策略来完成的,这将使子计算mergeSort' x与mergeSort' y并行发生。然后,我们合并结果,并使用rdeepseq强制其评估。
data Tree a = Leaf a | Node (Tree a) (Tree a) deriving (Show)
instance NFData a => NFData (Tree a) where
rnf (Leaf v) = deepseq v ()
rnf (Node x y) = deepseq (x, y) ()
listToTree [] = error "listToTree -- empty list"
listToTree [x] = Leaf x
listToTree xs = uncurry Node $ listToTree *** listToTree $
splitAt (length xs `div` 2) xs
-- mergeSort' :: Ord a => Tree a -> Eval [a]
mergeSort' l (Leaf v) = return [v]
mergeSort' l (Node x y) = do
xr <- strat $ runEval $ mergeSort' (l - 1) x
yr <- rseq $ runEval $ mergeSort' (l - 1) y
rdeepseq (merge xr yr)
where
merge [] y = y
merge x [] = x
merge (x:xs) (y:ys) | x < y = x : merge xs (y:ys)
| otherwise = y : merge (x:xs) ys
strat | l > 0 = rpar
| otherwise = rseq
mergeSort = runEval . mergeSort' 10
只需对计算的几个级别进行评估,我们应该具有相当不错的并行通信复杂度--大约是n的一些常数因子次方。
结果
在此处获取第四版源代码[http://pastebin.com/DxYneAaC],并使用以下命令来检查线程使用情况,或者使用后续命令行进行基准测试:
rm -f ParallelMergeSort; ghc -O2 -O3 -optc-O3 -optc-ffast-math -eventlog --make -rtsopts -threaded ParallelMergeSort.hs
./ParallelMergeSort +RTS -H512m -K512m -ls -N
threadscope ParallelMergeSort.eventlog
在一台24核心的X5680 @ 3.33GHz上测试结果显示改进不大。
> ./ParallelMergeSort
initialization: 10.461204s sec.
sorting: 6.383197s sec.
> ./ParallelMergeSort +RTS -H512m -K512m -N
initialization: 27.94877s sec.
sorting: 5.228463s sec.
在我的机器上,使用的是四核Phenom II处理器。
> ./ParallelMergeSort
initialization: 18.943919s sec.
sorting: 10.465077s sec.
> ./ParallelMergeSort +RTS -H512m -K512m -ls -N
initialization: 22.92075s sec.
sorting: 7.431716s sec.
在Threadscope中检查结果,小数据量的利用率很高(但遗憾的是没有明显的加速)。然而,当我尝试在像上面那样的大型列表上运行它时,它一半时间使用约2个CPU。看起来有很多火花被修剪了。它还对内存参数敏感,256MB是最佳选择,128MB需要9秒,512MB需要8.4秒,1024MB需要12.3秒!
我正在寻找的解决方案
最后,如果有人知道一些高效工具可以应对这个问题,我会非常感激。(Eden?)我对Haskell并行性的主要兴趣是能够编写小型研究项目的辅助工具,我可以将其放在我们实验室群集的24或80个核心服务器上。由于它们不是我们小组研究的重点,所以我不想花费太多时间在并行化效率上。因此,对我来说,简单就是更好的,即使我只能得到20%的使用率。
进一步讨论
- 我注意到Threadscope中的第二个条形图有时是绿色的(参见其主页,在那里第二个条形图似乎总是垃圾回收)。这是什么意思?
- 有没有办法绕过垃圾回收?它似乎花费了很多时间。例如,为什么不能分叉一个子计算,将结果返回到共享内存中,然后死亡?
- 有没有更好的方法(箭头、应用程序)来表达并行性?
listToTree
的最后一个情况可以写作uncurry Node $ splitAt (length xs `div` 2) xs
。 - hammarlistToTree xs = uncurry Node $ listToTree *** listToTree $ splitAt (length xs \
div` 2) xs`。 - gatoatigradosplitAt
只会遍历一次列表。 - hammarrpar
,而在第二个计算中使用了rseq
。因为当您激活这两个时,merge
的计算会立即开始,然后您将有三个线程同时计算xr
和yr
。 - Peter Wortmann