44

我有一个用 Python 和 Haskell 编写的简单脚本。它读取包含 1,000,000 个换行符分隔的整数的文件,将该文件解析为整数列表,对其进行快速排序,然后将其写入已排序的不同文件。该文件与未排序的文件具有相同的格式。简单的。

这是哈斯克尔:

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

main = do
    file <- readFile "data"
    let un = lines file
    let f = map (\x -> read x::Int ) un
    let done = quicksort f
    writeFile "sorted" (unlines (map show done))

这是Python:

def qs(ar):
    if len(ar) == 0:
        return ar

    p = ar[0]
    return qs([i for i in ar if i < p]) + [p] + qs([i for i in ar if i > p])


def read_file(fn):
    f = open(fn)
    data = f.read()
    f.close()
    return data

def write_file(fn, data):
    f = open('sorted', 'w')
    f.write(data)
    f.close()


def main():
    data = read_file('data')

    lines = data.split('\n')
    lines = [int(l) for l in lines]

    done = qs(lines)
    done = [str(l) for l in done]

    write_file('sorted', "\n".join(done))

if __name__ == '__main__':
    main()

很简单。现在我编译 Haskell 代码

$ ghc -O2 --make quick.hs

我给这两个计时:

$ time ./quick
$ time python qs.py

结果:

哈斯克尔:

real    0m10.820s
user    0m10.656s
sys 0m0.154s

Python:

real    0m9.888s
user    0m9.669s
sys 0m0.203s

Python 怎么可能比本地代码 Haskell 更快?

谢谢

编辑

  • Python版本:2.7.1
  • GHC 版本:7.0.4
  • Mac OSX,10.7.3
  • 2.4GHz 英特尔酷睿 i5

列表由

from random import shuffle
a = [str(a) for a in xrange(0, 1000*1000)]
shuffle(a)
s = "\n".join(a)
f = open('data', 'w')
f.write(s)
f.close()

所以所有的数字都是唯一的。

4

7 回答 7

50

原始的 Haskell 代码

Haskell 版本有两个问题:

  • 您正在使用字符串 IO,它构建字符的链接列表
  • 您正在使用看起来像快速排序的非快速排序。

该程序在我的 Intel Core2 2.5 GHz 笔记本电脑上运行需要 18.7 秒。(使用 -O2 的 GHC 7.4)

Daniel 的 ByteString 版本

这得到了很大改进,但请注意它仍然使用低效的内置合并排序。

他的版本需要 8.1 秒(并且不处理负数,但这对于本次探索来说不是问题)。

笔记

从这里开始,这个答案使用以下包:Vectorattoparsec和. 另请注意,使用 timsort 的 kindall 版本在我的机器上需要 2.8 秒(编辑:使用 pypy 需要 2 秒)。textvector-algorithms

文字版

我撕掉了 Daniel 的版本,将其翻译为 Text(因此它可以处理各种编码),并使用VectorST monad 中的可变对象添加了更好的排序:

import Data.Attoparsec.Text.Lazy
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.IO as TIO
import qualified Data.Vector.Unboxed as V
import qualified Data.Vector.Algorithms.Intro as I
import Control.Applicative
import Control.Monad.ST
import System.Environment (getArgs)

parser = many (decimal <* char '\n')

main = do
    numbers <- TIO.readFile =<< fmap head getArgs
    case parse parser numbers of
        Done t r | T.null t -> writeFile "sorted" . unlines
                                                  . map show . vsort $ r
        x -> error $ Prelude.take 40 (show x)

vsort :: [Int] -> [Int]
vsort l = runST $ do
        let v = V.fromList l
        m <- V.unsafeThaw v
        I.sort m
        v' <- V.unsafeFreeze m
        return (V.toList v')

这在 4 秒内运行(并且也不处理底片)

返回字节串

所以现在我们知道我们可以制作一个更快的更通用的程序,那么让仅 ASCii 的版本更快呢?没问题!

import qualified Data.ByteString.Lazy.Char8 as BS
import Data.Attoparsec.ByteString.Lazy (parse,  Result(..))
import Data.Attoparsec.ByteString.Char8 (decimal, char)
import Control.Applicative ((<*), many)
import qualified Data.Vector.Unboxed as V
import qualified Data.Vector.Algorithms.Intro as I
import Control.Monad.ST


parser = many (decimal <* char '\n')

main = do
    numbers <- BS.readFile "rands"
    case parse parser numbers of
        Done t r | BS.null t -> writeFile "sorted" . unlines
                                                   . map show . vsort $ r

vsort :: [Int] -> [Int]
vsort l = runST $ do
        let v = V.fromList l
        m <- V.unsafeThaw v
        I.sort m
        v' <- V.unsafeFreeze m
        return (V.toList v')

这在 2.3 秒内运行。

生成测试文件

以防万一有人好奇,我的测试文件由以下人员生成:

import Control.Monad.CryptoRandom
import Crypto.Random
main = do
  g <- newGenIO :: IO SystemRandom
  let rs = Prelude.take (2^20) (map abs (crandoms g) :: [Int])
  writeFile "rands" (unlines $ map show rs)

如果您想知道为什么vsort不在 Hackage 上以更简单的形式打包......我也是。

于 2012-04-28T03:54:20.193 回答
41

简而言之,不要使用read. 替换read为这样的函数:

import Numeric

fastRead :: String -> Int
fastRead s = case readDec s of [(n, "")] -> n

我得到了相当公平的加速:

~/programming% time ./test.slow
./test.slow  9.82s user 0.06s system 99% cpu 9.901 total
~/programming% time ./test.fast
./test.fast  6.99s user 0.05s system 99% cpu 7.064 total
~/programming% time ./test.bytestring
./test.bytestring  4.94s user 0.06s system 99% cpu 5.026 total

只是为了好玩,上面的结果包括一个使用ByteStringULTIMATE BARE-METAL SPEED 的版本(因此通过完全忽略文件编码问题而未能通过“为 21 世纪做好准备”测试)。它还有一些其他差异;例如,它发送到标准库的排序功能。完整代码如下。

import qualified Data.ByteString as BS
import Data.Attoparsec.ByteString.Char8
import Control.Applicative
import Data.List

parser = many (decimal <* char '\n')

reallyParse p bs = case parse p bs of
    Partial f -> f BS.empty
    v -> v

main = do
    numbers <- BS.readFile "data"
    case reallyParse parser numbers of
        Done t r | BS.null t -> writeFile "sorted" . unlines . map show . sort $ r
于 2012-04-27T21:37:37.613 回答
30

与其说是 Haskellite,不如说是 Pythonista,但我会尝试一下:

  1. 在您测量的运行时中,仅读取和写入文件就有相当多的开销,这在两个程序之间可能非常相似。另外,请注意您已经为这两个程序预热了缓存。

  2. 您的大部分时间都花在制作列表和列表片段的副本上。Python 列表操作经过高度优化,是该语言中最常用的部分之一,并且列表推导通常也非常高效,它们将大部分时间花在 Python 解释器中的 C 语言上。在 Python 中速度慢但在静态语言中速度很快的东西并不多,例如对象实例上的属性查找。

  3. 您的 Python 实现会丢弃等于枢轴的数字,因此到最后它可能会排序更少的项目,从而具有明显的优势。(如果您正在排序的数据集中没有重复项,这不是问题。)修复此错误可能需要在每次调用 时对列表的大部分内容进行另一个副本qs(),这会使 Python 速度慢一点。

  4. 您没有提及您使用的是哪个版本的 Python。如果您使用的是 2.x,您可能只需切换到 Python 3.x 就可以让 Haskell 击败 Python。:-)

我对这两种语言在这里基本上并驾齐驱并不感到惊讶(10% 的差异并不值得注意)。使用 C 作为性能基准,Haskell 因其惰性函数性质而损失了一些性能,而 Python 由于是解释性语言而损失了一些性能。一场体面的比赛。

由于 Daniel Wagner 使用内置的 发布了一个优化的 Haskell 版本sort,这里有一个类似的优化 Python 版本,使用list.sort()

mylist = [int(x.strip()) for x in open("data")]
mylist.sort()
open("sorted", "w").write("\n".join(str(x) for x in mylist))

在我的机器上需要 3.5 秒,而原始代码大约需要 9 秒。几乎与优化的 Haskell 并驾齐驱。原因:它大部分时间都花在 C 编程库上。此外,TimSort(Python 中使用的排序)是一头野兽。

于 2012-04-27T21:30:15.557 回答
9

这是事后的事,但我认为大部分的麻烦在于 Haskell 的写作。下面的模块非常原始——应该使用构建器,当然也可以避免通过 String 进行荒谬的往返显示——但它很简单,并且明显优于使用 kindall 改进的 python 的 pypy,并且优于其他地方的 2 秒和 4 秒 Haskell 模块在这个页面上(令我惊讶的是他们使用列表的程度,所以我又转了几圈。)

$ time aa.hs        real    0m0.709s
$ time pypy aa.py   real    0m1.818s
$ time python aa.py real    0m3.103s

我正在使用推荐用于来自矢量算法的未装箱矢量的排序。以某种形式使用 Data.Vector.Unboxed 现在显然是做这类事情的标准、幼稚的方式——它是新的 Data.List(用于 Int、Double 等)sort。我认为这仍然可以得到很大的改进,特别是在写入端。从要求它打印一堆索引而不是写入文件中可以看出,读取和排序总共需要大约 0.2 秒,因此写入时间是其他时间的两倍。如果 pypy 大部分时间都在使用 timsort 或其他什么东西,那么看起来排序本身在 Haskell 中肯定要好得多,而且同样简单——如果你能掌握该死的向量……

我不确定为什么没有方便的函数来读取和写入自然格式的未装箱的东西的向量——如果有的话,这将是三行长,并且会避免 String 并且速度更快,但也许我只是没有没见过他们。

import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Char8 as B
import qualified Data.Vector.Unboxed.Mutable as M
import qualified Data.Vector.Unboxed as V
import Data.Vector.Algorithms.Radix 
import System.IO

main  = do  unsorted <- fmap toInts (BL.readFile "data")
            vec <- V.thaw unsorted
            sorted <- sort vec >> V.freeze vec
            withFile "sorted" WriteMode $ \handle ->
               V.mapM_ (writeLine handle) sorted

writeLine :: Handle -> Int -> IO ()
writeLine h int = B.hPut h $ B.pack (show int ++ "\n")

toInts :: BL.ByteString -> V.Vector Int
toInts bs = V.unfoldr oneInt (BL.cons ' ' bs) 

oneInt :: BL.ByteString -> Maybe (Int, BL.ByteString)
oneInt bs = if BL.null bs then Nothing else 
               let bstail = BL.tail bs
               in if BL.null bstail then Nothing else BL.readInt bstail
于 2012-05-03T17:22:52.637 回答
2

我注意到了一些其他人由于某种原因没有注意到的问题;你的haskell和python代码都有这个。(请告诉我它是否在自动优化中得到修复,我对优化一无所知)。为此,我将在 haskell 中演示。在您的代码中,您可以像这样定义较小和较大的列表:

where lesser = filter (<p) xs
      greater = filter (>=p) xs

这很糟糕,因为您将 xs 中的每个元素与 p 进行了两次比较,一次是为了进入较小的列表,另一次是为了进入较大的列表。这(理论上;我没有检查时间)使您的排序使用两倍的比较;这是一场灾难。相反,您应该创建一个函数,使用谓词将列表分成两个列表,以这样的方式

split f xs

相当于

(filter f xs, filter (not.f) xs)

使用这种函数,您只需比较列表中的每个元素一次即可知道将其放在元组的哪一侧。
好的,让我们这样做:

where
    split :: (a -> Bool) -> [a] -> ([a], [a])
    split _ [] = ([],[])
    split f (x:xs)
        |f x       = let (a,b) = split f xs in (x:a,b)
        |otherwise = let (a,b) = split f xs in (a,x:b)

现在让我们用

let (lesser, greater) = split (p>) xs in (insert function here)

完整代码:

quicksort :: Ord a => [a] -> [a]
quicksort []     = []
quicksort (p:xs) =
    let (lesser, greater) = splitf (p>) xs
    in (quicksort lesser) ++ [p] ++ (quicksort greater)
    where
        splitf :: (a -> Bool) -> [a] -> ([a], [a])
        splitf _ [] = ([],[])
        splitf f (x:xs)
            |f x       = let (a,b) = splitf f xs in (x:a,b)
            |otherwise = let (a,b) = splitf f xs in (a,x:b)

出于某种原因,我无法在 where 子句中纠正 getter/lesser 部分,所以我必须在 let 子句中纠正它。另外,如果它不是尾递归,请告诉我并为我修复它(我还不知道尾递归如何完全工作)

现在你应该对 python 代码做同样的事情。我不知道python,所以我不能为你做。

编辑: 实际上在 Data.List 中已经有这样的功能,称为分区。请注意,这证明了对这种函数的需要,因为否则它不会被定义。这将代码缩小为:

quicksort :: Ord a => [a] -> [a]
quicksort []     = []
quicksort (p:xs) =
    let (lesser, greater) = partition (p>) xs
    in (quicksort lesser) ++ [p] ++ (quicksort greater)
于 2014-01-28T22:00:19.890 回答
2

为了跟进@kindall 有趣的答案,这些时间取决于您使用的 python / Haskell 实现、运行测试的硬件配置以及两种语言的算法实现。

尽管如此,我们还是可以尝试获得一些关于一种语言实现与另一种语言或从一种语言到另一种语言的相对性能的良好提示。使用像 qsort 这样的众所周知的算法,这是一个好的开始。

为了说明 python/python 比较,我刚刚在同一台机器上的 CPython 2.7.3 和 PyPy 1.8 上测试了您的脚本:

  • CPython:~8s
  • PyPy:~2.5s

这表明语言实现还有改进的空间,也许编译的 Haskell 最多不能解释和编译你的相应代码。如果您在 Python 中寻找速度,如果需要并且您的覆盖代码允许您这样做,还可以考虑切换到 pypy。

于 2012-04-27T22:11:43.557 回答
1

Python 确实针对这类事情进行了优化。我怀疑 Haskell 不是。这是一个类似的问题,它提供了一些非常好的答案。

于 2012-04-27T21:13:33.287 回答