25

我对 Haskell 很陌生,我有一个问题,即使用不纯(可变)数据结构可以提高性能。我试图将我听到的一些不同的东西拼凑起来,所以如果我的术语不完全正确,或者有一些小错误,请多多包涵。

为了具体说明,请考虑快速排序算法(取自 Haskell wiki)。

quicksort :: Ord a => [a] -> [a]
quicksort []     = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
    where
        lesser  = filter (< p) xs
        greater = filter (>= p) xs

这不是“真正的快速排序”。“真正的”快速排序算法是就地的,但事实并非如此。这是非常低效的内存。

另一方面,可以在 Haskell 中使用向量来实现就地快速排序。这个stackoverflow答案中给出了一个例子。

第二个算法比第一个快多少?大 O 符号在这里没有帮助,因为性能改进将来自更有效地使用内存,而不是更好的算法(对吗?)。我厌倦了自己构建一些测试用例,但我很难让事情运行起来。

一个理想的答案会给出一些关于是什么让就地 Haskell 算法在理论上更快的想法,以及一些测试数据集上运行时间的示例比较。

4

2 回答 2

22

另一方面,可以在 Haskell 中使用向量来实现就地快速排序。

第二个算法比第一个快多少?

当然,这取决于实施。如下所示,对于不太短的列表,对可变向量或数组进行适当的就地排序比排序列表快得多,即使包括从列表到列表的转换时间(并且该转换弥补了大部分时间)。

但是,列表算法会产生增量输出,而数组/向量算法在完成之前不会产生任何结果,因此排序列表仍然是可取的。

我不确切知道链接的可变数组/向量算法做错了什么。但他们做错了事。

对于可变向量代码,它似乎使用了盒装向量,并且是多态的,两者都会对性能产生重大影响,尽管如果函数是{-# INLINABLE #-}.

对于IOUArray代码,好吧,它看起来很有趣,但速度很慢。它使用IORef,readArraywriteArray并没有明显的严格性。那么,它所花费的糟糕时间就不足为奇了。

使用, 对(单态)C 代码进行更直接的翻译STUArray,并使用包装器使其在列表中工作¹,

{-# LANGUAGE BangPatterns #-}
module STUQuickSort (stuquick) where

import Data.Array.Base (unsafeRead, unsafeWrite)
import Data.Array.ST
import Control.Monad.ST

stuquick :: [Int] -> [Int]
stuquick [] = []
stuquick xs = runST (do
    let !len = length xs
    arr <- newListArray (0,len-1) xs
    myqsort arr 0 (len-1)
    -- Can't use getElems for large arrays, that overflows the stack, wth?
    let pick acc i
            | i < 0     = return acc
            | otherwise = do
                !v <- unsafeRead arr i
                pick (v:acc) (i-1)
    pick [] (len-1))

myqsort :: STUArray s Int Int -> Int -> Int -> ST s ()
myqsort a lo hi
   | lo < hi   = do
       let lscan p h i
               | i < h = do
                   v <- unsafeRead a i
                   if p < v then return i else lscan p h (i+1)
               | otherwise = return i
           rscan p l i
               | l < i = do
                   v <- unsafeRead a i
                   if v < p then return i else rscan p l (i-1)
               | otherwise = return i
           swap i j = do
               v <- unsafeRead a i
               unsafeRead a j >>= unsafeWrite a i
               unsafeWrite a j v
           sloop p l h
               | l < h = do
                   l1 <- lscan p h l
                   h1 <- rscan p l1 h
                   if (l1 < h1) then (swap l1 h1 >> sloop p l1 h1) else return l1
               | otherwise = return l
       piv <- unsafeRead a hi
       i <- sloop piv lo hi
       swap i hi
       myqsort a lo (i-1)
       myqsort a (i+1) hi
   | otherwise = return ()

以及对未装箱向量的良好排序(Introsort,而不是快速排序)的包装器,

module VSort where

import Data.Vector.Algorithms.Intro
import qualified Data.Vector.Unboxed as U
import Control.Monad.ST

vsort :: [Int] -> [Int]
vsort xs = runST (do
    v <- U.unsafeThaw $ U.fromList xs
    sort v
    s <- U.unsafeFreeze v
    return $ U.toList s)

我得到的时间更符合预期(注意:对于这些时间,在调用排序算法之前已经deepseq编辑了随机列表。没有它,转换为 anSTUArray会慢得多,因为它会首先评估一长串 thunk确定长度。vectorfromList包的转换不存在这个问题。将 移到转换为,其他排序[和转换,在vector情况下]算法花费的时间要少一点,所以vector之间的区别-algorithms的 introsort 和快速排序变得更大一些。):deepseqSTUArraySTUArray

list size: 200000                    -O2     -fllvm  -fllvm-O2
────────             ────────   ────────   ────────   ────────
Data.List.sort      0.663501s  0.665482s  0.652461s  0.792005s
Naive.quicksort     0.587091s  0.577796s  0.585754s  0.667573s
STUArray.quicksort   1.58023s  0.142626s  1.597479s  0.156411s
VSort.vsort         0.820639s  0.139967s  0.888566s  0.143918s

没有优化的时代对STUArray. unsafeRead并且unsafeWrite必须内联才能快速。如果没有内联,您将获得每次调用的字典查找。因此对于大型数据集,我省略了未优化的方法:

list size: 3000000         -O2   -fllvm-O2
────────              ────────    ────────
Data.List.sort      16.728576s  16.442377s
Naive.quicksort     14.297534s  12.253071s
STUArray.quicksort   2.307203s   2.200807s
VSort.vsort          2.069749s   1.921943s

您可以看到,如果操作正确,对可变未装箱数组进行就地排序比基于列表的排序要快得多排序和未装箱可变向量上的排序之间的差异STUArray是由于算法不同还是这里的向量确实更快,我不知道。由于我从未观察到向量比STUArrays 更快²,我倾向于相信前者。STUArray快速排序和 introsort 之间的区别部分是由于矢量包提供的更好的列表之间的转换,部分原因是不同的算法。


Louis Wasserman的建议下,我使用矢量算法包中的其他排序算法运行了一个快速基准测试,并使用了一个不太大的数据集。结果并不令人惊讶,良好的通用算法 heapsort、introsort 和 mergesort 都做得很好,时间接近未装箱可变数组上的快速排序(但当然,快速排序在几乎排序的输入上会降级为二次行为,而这些保证 O(n*log n) 最坏情况)。专用排序算法AmericanFlag和基数排序做得不好,因为输入不适合他们的目的(基数排序在较大范围的较大输入上会做得更好,因为它比数据所需的传递次数要多得多)。由于其二次行为,插入排序是迄今为止最糟糕的。

AmericanFlag:
list size: 300000         -O2  -fllvm-O2
────────             ────────   ────────
Data.List.sort      1.083845s  1.084699s
Naive.quicksort     0.981276s   1.05532s
STUArray.quicksort  0.218407s  0.215564s
VSort.vsort         2.566838s  2.618817s

Heap:
list size: 300000         -O2  -fllvm-O2
────────             ────────   ────────
Data.List.sort      1.084252s   1.07894s
Naive.quicksort     0.915984s  0.887354s
STUArray.quicksort  0.219786s  0.225748s
VSort.vsort         0.213507s   0.20152s

Insertion:
list size: 300000         -O2   -fllvm-O2
────────             ────────    ────────
Data.List.sort      1.168837s   1.066058s
Naive.quicksort     1.081806s   0.879439s
STUArray.quicksort  0.241958s   0.209631s
VSort.vsort         36.21295s  27.564993s

Intro:
list size: 300000         -O2  -fllvm-O2
────────             ────────   ────────
Data.List.sort       1.09189s  1.112415s
Naive.quicksort     0.891161s  0.989799s
STUArray.quicksort  0.236596s  0.227348s
VSort.vsort         0.221742s   0.20815s

Merge:
list size: 300000         -O2  -fllvm-O2
────────             ────────   ────────
Data.List.sort      1.087929s  1.074926s
Naive.quicksort     0.875477s  1.019984s
STUArray.quicksort  0.215551s  0.221301s
VSort.vsort         0.236661s  0.230287s

Radix:
list size: 300000         -O2  -fllvm-O2
────────             ────────   ────────
Data.List.sort      1.085658s  1.085726s
Naive.quicksort     1.002067s  0.900985s
STUArray.quicksort  0.217371s  0.228973s
VSort.vsort         1.958216s  1.970619s

结论:除非您有特定的理由不这样做,否则推荐使用vector-algorithms中的一种良好的通用排序算法,以及在必要时从列表转换为列表的包装器,这是对大型列表进行排序的推荐方法。(这些算法也适用于盒装向量,在我的测量中比未盒装的向量慢大约 50%。)对于短列表,转换的开销会很大,以至于它不会支付。


现在,在@applicative 的建议下,看看vector-algorithms ' introsort 的排序时间,对未装箱向量的快速排序和改进的(无耻地窃取实现unstablePartition)快速排序STUArray

改进的STUArray快速排序:

{-# LANGUAGE BangPatterns #-}
module NQuick (stuqsort) where


import Data.Array.Base (unsafeRead, unsafeWrite, getNumElements)
import Data.Array.ST
import Control.Monad.ST
import Control.Monad (when)

stuqsort :: STUArray s Int Int -> ST s ()
stuqsort arr = do
    n <- getNumElements arr
    when (n > 1) (myqsort arr 0 (n-1))

myqsort :: STUArray s Int Int -> Int -> Int -> ST s ()
myqsort a lo hi = do
    p <- unsafeRead a hi
    j <- unstablePartition (< p) lo hi a
    h <- unsafeRead a j
    unsafeWrite a j p
    unsafeWrite a hi h
    when (j > lo+1) (myqsort a lo (j-1))
    when (j+1 < hi) (myqsort a (j+1) hi)

unstablePartition :: (Int -> Bool) -> Int -> Int -> STUArray s Int Int -> ST s Int
{-# INLINE unstablePartition #-}
unstablePartition f !lf !rg !v = from_left lf rg
  where
    from_left i j
      | i == j    = return i
      | otherwise = do
                      x <- unsafeRead v i
                      if f x
                        then from_left (i+1) j
                        else from_right i (j-1)

    from_right i j
      | i == j    = return i
      | otherwise = do
                      x <- unsafeRead v j
                      if f x
                        then do
                               y <- unsafeRead v i
                               unsafeWrite v i x
                               unsafeWrite v j y
                               from_left (i+1) j
                        else from_right i (j-1)

向量快速排序:

module VectorQuick (vquicksort) where

import qualified Data.Vector.Unboxed.Mutable as UM
import qualified Data.Vector.Generic.Mutable as GM
import Control.Monad.ST
import Control.Monad (when)

vquicksort :: UM.STVector s Int -> ST s ()
vquicksort uv = do
    let li = UM.length uv - 1
        ui = UM.unsafeSlice 0 li uv
    p <- UM.unsafeRead uv li
    j <- GM.unstablePartition (< p) ui
    h <- UM.unsafeRead uv j
    UM.unsafeWrite uv j p
    UM.unsafeWrite uv li h
    when (j > 1) (vquicksort (UM.unsafeSlice 0 j uv))
    when (j + 1 < li) (vquicksort (UM.unsafeSlice (j+1) (li-j) uv))

计时码:

{-# LANGUAGE BangPatterns #-}
module Main (main) where

import System.Environment (getArgs)
import System.CPUTime
import System.Random
import Text.Printf

import Data.Array.Unboxed
import Data.Array.ST hiding (unsafeThaw)
import Data.Array.Unsafe (unsafeThaw)
import Data.Array.Base (unsafeAt, unsafeNewArray_, unsafeWrite)
import Control.Monad.ST
import Control.Monad

import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as UM

import NQuick
import VectorQuick
import qualified Data.Vector.Algorithms.Intro as I

nextR :: StdGen -> (Int, StdGen)
nextR = randomR (minBound, maxBound)

buildArray :: StdGen -> Int -> UArray Int Int
buildArray sg size = runSTUArray (do
    arr <- unsafeNewArray_ (0, size-1)
    let fill i g
            | i < size  = do
                let (r, g') = nextR g
                unsafeWrite arr i r
                fill (i+1) g'
            | otherwise = return arr
    fill 0 sg)

buildVector :: StdGen -> Int -> U.Vector Int
buildVector sg size = U.fromList $ take size (randoms sg)

time :: IO a -> IO ()
time action = do
    t0 <- getCPUTime
    action
    t1 <- getCPUTime
    let tm :: Double
        tm = fromInteger (t1 - t0) * 1e-9
    printf "%.3f ms\n" tm

stu :: UArray Int Int -> Int -> IO ()
stu ua sz = do
    let !sa = runSTUArray (do
                st <- unsafeThaw ua
                stuqsort st
                return st)
    forM_ [0, sz `quot` 2, sz-1] (print . (sa `unsafeAt`))

intro :: U.Vector Int -> Int -> IO ()
intro uv sz = do
    let !sv = runST (do
            st <- U.unsafeThaw uv
            I.sort st
            U.unsafeFreeze st)
    forM_ [0, sz `quot` 2, sz-1] (print . U.unsafeIndex sv)

vquick :: U.Vector Int -> Int -> IO ()
vquick uv sz = do
    let !sv = runST (do
            st <- U.unsafeThaw uv
            vquicksort st
            U.unsafeFreeze st)
    forM_ [0, sz `quot` 2, sz-1] (print . U.unsafeIndex sv)

main :: IO ()
main = do
    args <- getArgs
    let !num = case args of
                 (a:_) -> read a
                 _ -> 1000000
    !sg <- getStdGen
    let !ar = buildArray sg num
        !vc = buildVector sg num
        !v2 = buildVector sg (foo num)
        algos = [ ("Intro", intro v2), ("STUArray", stu ar), ("Vquick", vquick vc) ]
    printf "Created data to be sorted, last elements %d %d %d\n" (ar ! (num-1)) (vc U.! (num-1)) (v2 U.! (num-1))
    forM_ algos $ \(name, act) -> do
        putStrLn name
        time (act num)

-- For the prevention of sharing
foo :: Int -> Int
foo n
    | n < 0 = -n
    | n > 0 = n
    | otherwise = 3

结果(仅次):

$ ./timeSorts 3000000
Intro
587.911 ms
STUArray
402.939 ms
Vquick
414.936 ms
$ ./timeSorts 1000000
Intro
193.970 ms
STUArray
131.980 ms
Vquick
134.979 ms

STUArray正如预期的那样,在和未装箱的向量上几乎相同的快速排序几乎花费了相同的时间。(旧的快速排序实现比 introsort 慢约 15%。与上述时间相比,大约 70-75% 用于从列表转换到列表。)

在随机输入上,快速排序的性能明显优于 introsort,但在几乎排序的输入上,它们的性能会下降,而 introsort 不会。


¹ 用 s 使代码多态STUArray充其量是一件痛苦的事,用IOUArrays 做这件事并让排序和包装器{-# INLINABLE #-}在优化的情况下产生相同的性能 - 如果没有,多态代码会明显变慢。

² 使用相同的算法,当我比较时(不经常),两者在测量精度范围内总是同样快。

于 2012-07-14T16:14:45.413 回答
18

没有什么比考试更好的了,对吧?结果并不令人惊讶:对于 range 中的随机整数列表[0 .. 1000000]

list size: 200000         ghc              -O2     -fllvm  -fllvm-O2
────────                   ────────   ────────   ────────   ────────
Data.List.sort            0.878969s  0.883219s  0.878106s  0.888758s
Naïve.quicksort           0.711305s  0.870647s  0.845508s  0.919925s
UArray_IO.quicksort       9.317783s  1.919583s  9.390687s  1.945072s
Vector_Mutable.quicksort   1.48142s  0.823004s  1.526661s  0.806837s

在这里,Data.List.sort就是它的本质,Naïve.quicksort是您引用的算法,UArray_IO.quicksort并且Vector_Mutable.quicksort取自您链接到的问题:klapauciusDan Burton 的答案 ,结果证明在性能方面非常不理想,看看Daniel Fischer 可以做得更好,都包装以便接受列表(不确定我是否完全正确):

quicksort :: [Int] -> [Int]
quicksort l = unsafePerformIO $ do
  let bounds = (0, length l)
  arr <- newListArray bounds l :: IO (IOUArray Int Int)
  uncurry (qsort arr) bounds
  getElems arr

quicksort :: Ord a => [a] -> [a]
quicksort = toList . iqsort . fromList

分别。

Data.Vector如您所见,在对随机生成的整数列表进行排序的速度方面,朴素算法与可变解决方案相差不远,IOUArray实际上更糟。测试是在运行 Ubuntu 11.10 x86-64 的 Intel i5 笔记本电脑上进行的。


考虑到 ɢᴏᴏᴅ 可变实现毕竟仍然远远领先于此处比较的所有实现,因此以下内容并没有多大意义。

请注意,这并不意味着一个好的基于列表的程序可以始终跟上其可变实现的等效程序,但 GHC 确实在使性能接近方面做得很好。此外,它当然取决于数据:这些时间是随机生成的要排序的列表包含 0 到 1000 之间的值,而不是上面的 0 到 1000000 之间的值,即有很多重复项:

list size: 200000         ghc               -O2      -fllvm  -fllvm-O2
────────                    ────────   ────────    ────────   ────────
Data.List.sort             0.864176s  0.882574s   0.850807s  0.857957s
Naïve.quicksort            1.475362s  1.526076s   1.475557s  1.456759s
UArray_IO.quicksort       24.405938s  5.255001s  23.561911s  5.207535s
Vector_Mutable.quicksort   3.449168s  1.125788s   3.202925s  1.117741s

更不用说预排序的数组了。

非常有趣的是(只有在非常大的尺寸下才变得明显,这需要 rtsopts 来增加堆栈容量)是两个可变实现如何变得显着变慢-fllvm -O2

list size: 3⋅10⁶        ghc      -O1   -fllvm-O1         -O2   -fllvm-O2
────────                    ────────    ────────    ────────    ────────
Data.List.sort            23.897897s  24.138117s  23.708218s  23.631968s
Naïve.quicksort           17.068644s  19.547817s  17.640389s  18.113622s
UArray_IO.quicksort       35.634132s  38.348955s  37.177606s  49.190503s
Vector_Mutable.quicksort  17.286982s  17.251068s  17.361247s  36.840698s

对我来说,不可变实现在 llvm 上的表现更好似乎是合乎逻辑的(它不是在某种程度上做到了所有事情都是不可变的吗?),尽管我不明白为什么这只会在高度优化时对可变版本的放缓变得明显和大数据量。


测试程序:

$ cat QSortPerform.hs
module Main where

import qualified Data.List(sort)
import qualified Naïve
import qualified UArray_IO
import qualified Vector_Mutable

import Control.Monad
import System.Random
import System.Environment

sortAlgos :: [ (String, [Int]->[Int]) ]
sortAlgos = [ ("Data.List.sort", Data.List.sort)
            , ("Naïve.quicksort", Naïve.quicksort)
            , ("UArray_IO.quicksort", UArray_IO.quicksort)
            , ("Vector_Mutable.quicksort", Vector_Mutable.quicksort) ]

main = do
   args <- getArgs
   when (length args /= 2) $ error "Need 2 arguments"

   let simSize = read $ args!!1
   randArray <- fmap (take simSize . randomRs(0,1000000)) getStdGen

   let sorted = case filter ((== args!!0) . fst) sortAlgos of
        [(_, algo)] -> algo randArray
        _ -> error $ "Argument must be one of " 
                        ++ show (map fst sortAlgos)

   putStr "First element:  "; print $ sorted!!0
   putStr "Middle element: "; print $ sorted!!(simSize`div`2)
   putStr "Last element:   "; print $ sorted!!(simSize-1)

它在命令行上采用算法名称和数组大小。运行时比较是用这个程序完成的:

$ cat PerformCompare.hs
module Main where

import System.Process
import System.Exit
import System.Environment
import Data.Time.Clock
import Data.List
import Control.Monad
import Text.PrettyPrint.Boxes

compiler = "ghc"
testProgram = "./QSortPerform"
flagOpts = [[], ["-O2"], ["-fllvm"], ["-fllvm","-O2"]]
algos = ["Data.List.sort","Naïve.quicksort","UArray_IO.quicksort","Vector_Mutable.quicksort"]


main = do
   args <- getArgs
   let testSize = case args of
         [numb] -> read numb
         _      -> 200000

   results <- forM flagOpts $ \flags -> do

      compilerExitC <- verboseSystem
              compiler $ testProgram : "-fforce-recomp" : flags
      when (compilerExitC /= ExitSuccess) .
         error $ "Compiler error \"" ++ show compilerExitC ++"\""

      algoCompare <- forM algos $ \algo -> do
         startTime <- getCurrentTime
         exitC <- verboseSystem testProgram [algo, show testSize]
         endTime <- getCurrentTime
         when (exitC /= ExitSuccess) .
            error $ "Program error \"" ++ show exitC ++"\""
         return . text . show $ diffUTCTime endTime startTime

      return . vcat right $ text(concat flags)
                          : text("────────")
                          : algoCompare

   let table = hsep 2 bottom
         $ vcat left (map text $ ("list size: "++show testSize)
                               : "────────"
                               : algos                          )
         : results

   printBox table



verboseSystem :: String -> [String] -> IO ExitCode
verboseSystem cmd args = do
   putStrLn . unwords $ cmd : args
   rawSystem cmd args
于 2012-07-14T11:03:11.953 回答