【发布时间】:2013-11-14 04:50:21
【问题描述】:
我得到了这个看似微不足道的并行快速排序实现,代码如下:
import System.Random
import Control.Parallel
import Data.List
quicksort :: Ord a => [a] -> [a]
quicksort xs = pQuicksort 16 xs -- 16 is the number of sparks used to sort
-- pQuicksort, parallelQuicksort
-- As long as n > 0 evaluates the lower and upper part of the list in parallel,
-- when we have recursed deep enough, n==0, this turns into a serial quicksort.
pQuicksort :: Ord a => Int -> [a] -> [a]
pQuicksort _ [] = []
pQuicksort 0 (x:xs) =
let (lower, upper) = partition (< x) xs
in pQuicksort 0 lower ++ [x] ++ pQuicksort 0 upper
pQuicksort n (x:xs) =
let (lower, upper) = partition (< x) xs
l = pQuicksort (n `div` 2) lower
u = [x] ++ pQuicksort (n `div` 2) upper
in (par u l) ++ u
main :: IO ()
main = do
gen <- getStdGen
let randints = (take 5000000) $ randoms gen :: [Int]
putStrLn . show . sum $ (quicksort randints)
我用编译
ghc --make -threaded -O2 quicksort.hs
并运行
./quicksort +RTS -N16 -RTS
无论我做什么,我都无法让它比在一个 cpu 上运行的简单顺序实现运行得更快。
- 能否解释为什么它在多个 CPU 上运行比在一个 CPU 上慢得多?
- 是否有可能通过一些技巧使这种规模(至少亚线性)与 CPU 的数量一致?
编辑:@tempestadept 暗示快速排序是问题所在。为了检查这一点,我以与上面示例相同的精神实现了一个简单的合并排序。它具有相同的行为,您添加的功能越多,执行速度就越慢。
import System.Random
import Control.Parallel
splitList :: [a] -> ([a], [a])
splitList = helper True [] []
where helper _ left right [] = (left, right)
helper True left right (x:xs) = helper False (x:left) right xs
helper False left right (x:xs) = helper True left (x:right) xs
merge :: (Ord a) => [a] -> [a] -> [a]
merge xs [] = xs
merge [] ys = ys
merge (x:xs) (y:ys) = case x<y of
True -> x : merge xs (y:ys)
False -> y : merge (x:xs) ys
mergeSort :: (Ord a) => [a] -> [a]
mergeSort xs = pMergeSort 16 xs -- we use 16 sparks
-- pMergeSort, parallel merge sort. Takes an extra argument
-- telling how many sparks to create. In our simple test it is
-- set to 16
pMergeSort :: (Ord a) => Int -> [a] -> [a]
pMergeSort _ [] = []
pMergeSort _ [a] = [a]
pMergeSort 0 xs =
let (left, right) = splitList xs
in merge (pMergeSort 0 left) (pMergeSort 0 right)
pMergeSort n xs =
let (left, right) = splitList xs
l = pMergeSort (n `div` 2) left
r = pMergeSort (n `div` 2) right
in (r `par` l) `pseq` (merge l r)
ris :: Int -> IO [Int]
ris n = do
gen <- getStdGen
return . (take n) $ randoms gen
main = do
r <- ris 100000
putStrLn . show . sum $ mergeSort r
【问题讨论】:
-
请注意,这实际上是一个快速排序的实现:stackoverflow.com/questions/7717691/…
-
至少我无法让它在使用
pseq时表现得更好,即使在使用sum清除任何可能的thunk 时也是如此。也许涉及到一个完全不同的问题。 — 正如我现在已通过回答删除,这里再次作为评论: 1. 将该函数命名为quicksort可能会造成混淆,因为您不希望这样的函数接受额外的并行参数; 2. 使用类型签名,仅always 用于顶级函数,当它们的工作方式可能与名称所暗示的略有不同时更是如此; 3.尽可能使用partition等库函数。 — 好问题,顺便说一句。 -
我没有足够的时间来发布完整的答案,但我想有两个可能的问题:(1)您应该使用
l `par` u `pseq` (u ++ l)。 (2) 当您并行运行子计算时,直到需要时才真正评估它们。因此,您应该将每个子列表强制为 NF(或至少其完整结构),例如forceList l `par` forceList u `pseq` (u ++ l)其中forceList是(您自己的)强制评估列表的函数。另外,为了进行适当的基准测试,我建议使用criterion。 -
如果您想要一种快速简便的方法来查看 Spark 的运行情况,可以使用
-rtsopts标志进行编译,然后在运行程序时添加-sstderr标志。跨度> -
实际上,只要我使用的线程数不超过内核数,mergesort 实现在我的机器上以几乎恒定的速度执行。我开始认为我们遇到的主要问题与内存/缓存有关。列表在这方面并不是很好。如果 所有 核心大部分时间都在等待获取内存页面,那么并行性几乎不会获得什么。在快速排序中,这显然比在归并排序中更为关键。
标签: haskell parallel-processing profiling quicksort