Python比编译Haskell更快?

Python faster than compiled Haskell?

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

这是哈斯克尔:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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('
'
)
    lines = [int(l) for l in lines]

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

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

if __name__ == '__main__':
    main()

很简单。现在我用

1
$ ghc -O2 --make quick.hs

我给他们两个计时:

1
2
$ time ./quick
$ time python qs.py

结果:

Haskell:

1
2
3
real    0m10.820s
user    0m10.656s
sys 0m0.154s

Python:

1
2
3
real    0m9.888s
user    0m9.669s
sys 0m0.203s

python怎么可能比本地代码haskell更快呢?

谢谢

编辑:

  • python版本:2.7.1
  • GHC版本:7.0.4
  • Mac OSX,107.3
  • 2.4GHz Intel Core i5

列表生成者

1
2
3
4
5
6
7
8
from random import shuffle
a = [str(a) for a in xrange(0, 1000*1000)]
shuffle(a)
s ="
"
.join(a)
f = open('data', 'w')
f.write(s)
f.close()

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


最初的哈斯克尔密码

Haskell版本有两个问题:

  • 您使用的是字符串IO,它构建了字符的链接列表
  • 您使用的是看起来像QuickSort的非QuickSort。

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

丹尼尔的bytestring版本

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

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

注释

从这里开始,答案使用以下包:Vectorattoparsectextvector-algorithms。还要注意,Kindall使用Timsort的版本在我的机器上需要2.8秒(编辑:使用Pypy需要2秒)。

文本版本

我摘掉了丹尼尔的版本,将其翻译成文本(这样它就可以处理各种编码),并在一个st monad中使用一个可变的Vector添加了更好的排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 '
'
)

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版本更快呢?没问题!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 '
'
)

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秒内运行。

生成测试文件

为了以防万一有人好奇,我的测试文件是由:

1
2
3
4
5
6
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没有在黑客时代以更简单的形式打包…我也是。


简而言之,不要使用read。用如下功能替换read

1
2
3
4
import Numeric

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

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

1
2
3
4
5
6
~/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

为了好玩,上面的结果包括一个使用ByteString的版本(因此完全忽略了文件编码的问题,未能通过"为21世纪做好准备"的测试),以获得最终的裸机速度。它还有一些其他的不同之处,例如,它提供给标准库的排序功能。完整代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import qualified Data.ByteString as BS
import Data.Attoparsec.ByteString.Char8
import Control.Applicative
import Data.List

parser = many (decimal <* char '
'
)

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


更像是一个Python而不是哈斯卡利特,但我要刺伤:

  • 在度量的运行时中,仅仅读写文件就有相当一部分开销,这两个程序之间可能非常相似。另外,请注意,您已经为这两个程序预热了缓存。

  • 你的大部分时间都花在制作列表和列表片段的副本上。python list操作经过了大量的优化,是语言中最常用的部分之一,而list理解通常也能很好地执行,在python解释器的C-land中花费了大量时间。在python中没有很多东西是缓慢的,但在静态语言中却是非常快的,比如对象实例的属性查找。

  • 您的python实现丢弃了与pivot相等的数字,因此到最后,它可能会对更少的项目进行排序,这给了它明显的优势。(如果要排序的数据集中没有重复项,这不是问题。)修复此错误可能需要在每次调用qs()时对列表的大部分进行另一次复制,这将使python的速度慢一点。

  • 你没有提到你使用的是什么版本的python。如果您使用的是2.x,那么只要切换到python 3.x.:-,就可以让haskell打败python。

  • 我不太惊讶这两种语言基本上是并驾齐驱的(10%的差异不值得注意)。使用C作为性能基准,haskell由于其懒散的功能性而损失了一些性能,而python由于是一种解释语言而损失了一些性能。一场像样的比赛

    由于Daniel Wagner使用内置的sort发布了一个优化的haskell版本,下面是一个使用list.sort()的类似优化的python版本:

    1
    2
    3
    4
    mylist = [int(x.strip()) for x in open("data")]
    mylist.sort()
    open("sorted","w").write("
    "
    .join(str(x) for x in mylist))

    在我的机器上是3.5秒,而原始代码大约是9秒。几乎仍然与优化哈斯克尔颈部和颈部。原因:它大部分时间都在C程序库中度过。另外,timsort(在python中使用的排序)是一种野兽。


    这是在事实之后,但我认为大部分的麻烦在于哈斯克尔的写作。下面的模块非常原始——应该使用构建器,当然也应该避免通过字符串进行的荒谬的往返显示——但是它很简单,并且明显优于pypy和kindall改进的python,并且优于本页其他地方的2秒和4秒haskell模块(这让我很惊讶他们使用了listS,所以我又转动了几圈曲柄。)

    1
    2
    3
    $ 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以外的所有东西都是令人恼火的IO管理,特别是在写端,我认为这仍然可以得到很大的改进。阅读和排序需要大约0.2秒,从要求它打印一堆索引中的内容而不是写入文件中可以看出,因此花费在写东西上的时间是其他任何东西的两倍。如果Pypy大部分时间都在使用Timsort或其他工具,那么它看起来在Haskell中的排序本身肯定要好得多,而且也很简单——如果你能把手放在darned向量上……

    我不知道为什么周围没有方便的函数来读取和写入自然格式的未绑定对象的向量——如果有的话,这将是三行长,避免使用字符串,速度更快,但也许我只是没有看到它们。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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 ++"
    "
    )

    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

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

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

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

    1
    split f xs

    等于

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

    使用这种函数,您只需要对列表中的每一个元素进行一次比较,就可以知道将它放在元组的哪一边。好吧,我们开始吧:

    1
    2
    3
    4
    5
    6
    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)

    现在让我们用较小/较大的发电机替换

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

    完整代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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/less部分,所以我必须在let子句中纠正它。另外,如果不是tail递归,请让我知道并为我修复它(我还不知道tail recorsive是如何完全工作的)

    现在,您应该对Python代码执行同样的操作。我不认识Python,所以我不能帮你。

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

    1
    2
    3
    4
    5
    quicksort :: Ord a => [a] -> [a]
    quicksort []     = []
    quicksort (p:xs) =
        let (lesser, greater) = partition (p>) xs
        in (quicksort lesser) ++ [p] ++ (quicksort greater)


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

    然而,我们可以尝试获得一些关于一种语言实现与另一种语言或从一种语言到另一种语言的相对性能的好提示。有了像qsort这样的著名算法,这是一个好的开始。

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

    • CPython:~8秒
    • PyPy:~2.5秒

    这表明在语言实现方面可能还有改进的空间,也许编译后的haskell并没有在最好的情况下对相应代码进行解释和编译。如果您要在Python中搜索速度,还可以考虑在需要时切换到pypy,如果您的覆盖代码允许您这样做的话。


    python确实针对这类事情进行了优化。我怀疑哈斯克尔不是。这里有一个类似的问题,提供了一些非常好的答案。