【问题标题】:Python faster than compiled Haskell?Python 比已编译的 Haskell 更快?
【发布时间】:2012-04-27 20:51:20
【问题描述】:

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

这是 Haskell:

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()

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

【问题讨论】:

  • python 实现中的错误?您正在使用i &gt; pi &lt; p 构建子列表。 i = p的时候呢?
  • 好吧,我猜大部分 Python 运行时间都花在了连接 Python 列表(动态数组,以防有人不知道)的 C 代码中。 cProfile 应该会告诉你这是不是真的。
  • 9.9 比 10.8 快多少?一个简单的高清读取错误可能会导致十倍的差异。
  • 您使用的是哪个版本的 GHC 和 Python?当我尝试运行您的 Python 代码时出现异常(Python 2.6.5 --- 我不是 Pythonista)。
  • 请注意,您主要是在计时 I/O 以及在整数和字符串之间进行转换。用id 替换对quicksort 的调用只会将我机器上的运行时间减少约30%。

标签: python haskell quicksort


【解决方案1】:

原始 Haskell 代码

Haskell 版本有两个问题:

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

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

Daniel 的 ByteString 版本

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

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

注意

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

文字版

我抄袭了 Daniel 的版本,将其翻译为 Text(因此它可以处理各种编码),并在 ST monad 中使用可变的 Vector 添加了更好的排序:

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 上以更简单的形式打包......我也是。

【讨论】:

【解决方案2】:

简而言之,不要使用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

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

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

【讨论】:

  • 编译这个我需要做些什么特别的事情吗?我收到Could not find module Data.Attoparsec.ByteString.Char8'`
  • @HonzaPokorny 是的,cabal install attoparsec 从你的命令行开始。
  • 编译得很好,但对我不起作用。投诉Non-exhaustive patterns in case。我不是真正的 Haskeller。
  • @HonzaPokorny 文件可能与解析器识别的格式不完全一致——也许你在 Windows 上,或者文件末尾没有换行符,或者其他类似的东西。如果没有实际的文件(而不是,例如,复制和粘贴到在线粘贴箱的结果),基本上是不可能调试的。
  • Python 2,当在 OP 的脚本中使用时,也会对编码产生影响,所以我想这是公平的 :)
【解决方案3】:

比 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 发布了一个使用内置 sort 优化的 Haskell 版本,这里有一个使用 list.sort() 的类似优化的 Python 版本:

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 中使用的排序)是一个野兽。

【讨论】:

  • 很好的观察,关于 #4 的只有一件事:他确实命名了编译器和标志:ghc -O2 --make quick.hs
  • 嗯,Python 和 Haskell 现在的速度接近相同了,所以如果你切换到较慢的 Python 版本(例如 3.x),Python 版本可能会落后于 Haskell。
  • 好吧,这正是我想知道的。为什么 python 3 比 python 2 慢?你有什么参考资料/材料我可以看看吗?
  • Python 3 有两个主要的性能瓶颈:所有整数都是长整数,用它来做数学运算比较慢(pystone 3.0 比 2.6 慢 20%),所有字符串都是 Unicode,使用速度较慢,并且需要对所有 I/O 进行编码/解码(Web 应用程序基准测试每秒请求减少 15%)。不过,在某些操作上,Python 3 似乎实际上更快,而且我相信最近已经做了一些工作来缩小差距。 (我从mikewatkins.ca/2008/12/06/python-3-performance-a-red-herring 获得了上述数字,但谷歌“Python 3 性能”等)
【解决方案4】:

这是事后的事,但我认为大部分的麻烦在于 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 之外的所有东西都令人讨厌IO 管理,我认为仍然可以大幅改进,尤其是在写入端。从要求它打印一堆索引而不是写入文件中可以看出,读取和排序总共需要大约 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

【讨论】:

    【解决方案5】:

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

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

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

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

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

    【讨论】:

      【解决方案6】:

      我注意到了一些其他人由于某种原因没有注意到的问题;你的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)
      

      【讨论】:

        【解决方案7】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-05-24
          • 2018-07-08
          • 2010-10-13
          • 1970-01-01
          相关资源
          最近更新 更多