簡體   English   中英

Python比編譯的Haskell快嗎?

[英]Python faster than compiled Haskell?

我有一個用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

結果:

Haskell:

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

蟒蛇:

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

因此,所有數字都是唯一的。

原始的Haskell密碼

Haskell版本存在兩個問題:

  • 您正在使用字符串IO,該字符串將構建鏈接的字符列表
  • 您正在使用看起來像快速排序的非快速排序。

在我的Intel Core2 2.5 GHz筆記本電腦上運行該程序需要18.7秒。 (GHC 7.4使用-O2)

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上...我也是。

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

與Haskellite相比,它更像一個Pythonista,但我會刺一針:

  1. 在測量的運行時中,僅讀取和寫入文件就有相當大的開銷,這在兩個程序之間可能非常相似。 另外,請注意,您已經預熱了這兩個程序的緩存。

  2. 您的大部分時間都花在制作列表的副本和列表的片段上。 Python列表操作經過了高度優化,是該語言中最常用的部分之一,列表理解通常也很不錯,它們大部分時間都花在Python解釋器的C-land中。 在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中使用的排序)是一種野獸。

這是事實,但我認為大多數麻煩都在於Haskell寫作中。 以下模塊是非常原始的模塊-應該使用構建器,並且肯定會避免通過String進行的可笑的來回顯示-但它很簡單,並且比使用pyall改進的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

我正在使用推薦用於vector-algorithms中的未裝箱矢量的排序。 現在,以某種形式使用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

要跟蹤@kindall有趣的答案,這些時間取決於您使用的python / Haskell實現,運行測試所基於的硬件配置以及您所使用的兩種語言的算法實現。

不過,我們可以嘗試從某種語言實現相對於另一種實現,或者從一種語言轉換為另一種語言的相對性能方面獲得一些好的提示。 對於像qsort這樣的知名alogrithms,這是一個好的開始。

為了說明python / python的比較,我只是在同一台機器上的CPython 2.7.3和PyPy 1.8上測試了您的腳本:

  • CPython:〜8秒
  • PyPy:約2.5秒

這表明在語言實現上可能還有改進的余地,也許已編譯的Haskell最多沒有執行相應代碼的解釋和編譯。 如果要在Python中搜索速度,還可以考慮在需要時並且在覆蓋代碼允許的情況下切換到pypy。

我注意到一些其他人由於某種原因沒有注意到的問題; 您的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)

Python確實針對此類事情進行了優化。 我懷疑Haskell不是。 這是一個類似的問題 ,提供了很好的答案。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM