algorithm - 在 Haskell 中使用向量来提高性能

标签 algorithm haskell

我对 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 中使用向量来实现就地快速排序。在 this stackoverflow answer. 中给出了一个示例

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

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

最佳答案

没有什么比测试更好的了,对吧?结果并不令人意外:对于 [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.quicksortVector_Mutable.quicksort 取自您链接到的问题:klapaucius'Dan Burton's answer 结果证明在性能方面非常次优,看看有什么更好的包装以便接受列表(不确定我是否完全正确):
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

分别。

如您所见,在对随机生成的整数列表进行排序的速度方面,naïve 算法与 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

关于algorithm - 在 Haskell 中使用向量来提高性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11481675/

相关文章:

algorithm - 自由格式文本的通用地址解析器

algorithm - 如何复制包含循环的完整非二叉树

algorithm - 加权最小二乘 - 将平面拟合到 3D 点集

php - 检查图像是否存在,如果不存在,请使用 php 选择另一个图像

搜索数组中元素的算法

http - 从维基百科下载 pdf 文件

Haskell - 模式匹配重叠

haskell - f1 = 翻转常量映射。这个功能如何运作?

haskell - 为什么 Int 不实现 'Monoid' ?

haskell - SCons 和 Shake 的区别