44

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

そして、私はそれらの2つの時間を次のように計ります。

$ time ./quick
$ time python qs.py

結果:

Haskell:

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 Intel Core 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バージョンには2つの問題があります。

  • 文字のリンクリストを作成する文字列IOを使用しています
  • クイックソートのように見える非クイックソートを使用しています。

このプログラムは、Intel Core22.5GHzラップトップで実行するのに18.7秒かかります。(-O2を使用したGHC 7.4)

ダニエルのByteStringバージョン

これは大幅に改善されていますが、非効率的な組み込みのマージソートを使用していることに注意してください。

彼のバージョンは8.1秒かかります(そして負の数を処理しませんが、それはこの調査では問題ではありません)。

ノート

ここから、この回答では次のパッケージを使用します:Vector、、、。また、timsortを使用したkindallのバージョンは私のマシンで2.8秒かかることに注意してください(編集:およびpypyを使用して2秒)。attoparsectextvector-algorithms

テキストバージョン

ダニエルのバージョンを取り除いて、テキストに変換し(さまざまなエンコーディングを処理できるように)Vector、STモナドでミュータブルを使用してより適切な並べ替えを追加しました。

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)

なぜvsortHackageでもっと簡単な形でパッケージ化されていないのか疑問に思っているのなら、私もそうです。

于 2012-04-28T03:54:20.193 に答える
41

つまり、を使用しないでくださいreadread次のような関数に置き換えます。

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つのプログラム間でかなり似ています。また、両方のプログラムのキャッシュをウォームアップしていることに注意してください。

  2. ほとんどの時間は、リストのコピーとリストのフラグメントの作成に費やされます。Pythonリスト操作は高度に最適化されており、言語で最も頻繁に使用される部分の1つであり、リスト内包表記も通常はかなりパフォーマンスが高く、Pythonインタープリター内のCランドで多くの時間を費やしています。Pythonでは遅くなりますが、オブジェクトインスタンスでの属性ルックアップなど、静的言語では速く邪悪なものは多くありません。

  3. Pythonの実装では、ピボットに等しい数が破棄されるため、最終的にはソートするアイテムが少なくなり、明らかな利点が得られる可能性があります。(並べ替えるデータセットに重複がない場合、これは問題ではありません。)このバグを修正するには、の呼び出しごとにリストのほとんどのコピーを作成する必要があります。これにより、qs()Pythonの速度が少し低下します。

  4. 使用しているPythonのバージョンについては言及していません。2.xを使用している場合は、Python 3.xに切り替えるだけで、HaskellがPythonを打ち負かすことができるでしょう。:-)

ここでは、2つの言語が基本的に首を絞めていることにそれほど驚いていません(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

これは事後ですが、問題のほとんどはハスケルの執筆にあると思います。次のモジュールはかなり原始的です-おそらくビルダーを使用して、表示のために文字列を介したばかげたラウンドトリップを避ける必要があります-しかし、それはシンプルで、kindallの改良されたpythonを使用したpypyよりも明らかに優れており、他の場所の2秒と4秒のHaskellモジュールよりも優れていますこのページで(彼らがリストをどれだけ使用しているかに驚いたので、クランクをさらに2、3回転させました。)

$ 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です。これは、特に書き込み側では、まだ大幅に改善されていると思います。ファイルに書き込む代わりに、一連のインデックスにあるものを印刷するように要求することからわかるように、読み取りと並べ替えを一緒に行うには約0.2秒かかるため、他の何よりも2倍の時間が書き込みに費やされます。pypyがほとんどの時間をティムソートなどを使用して費やしている場合、Haskellではソート自体が確かに非常に優れているように見えます。

ボックス化されていないもののベクトルを自然な形式から読み書きするための便利な関数が周りにない理由はわかりません-もしあれば、これは3行の長さで、文字列を避けてはるかに高速になりますが、おそらく私はそれらを見ていません。

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

@kindallの興味深い答えをフォローアップするために、これらのタイミングは、使用するpython / Haskell実装、テストを実行するハードウェア構成、および両方の言語でのアルゴリズム実装の両方に依存します。

それでも、ある言語の実装の相対的なパフォーマンスを別の言語と比較したり、ある言語から別の言語に比較したりするための良いヒントを得ることができます。qsortのようなよく知られたアルゴリズムで、それは良い始まりです。

PythonとPythonの比較を説明するために、同じマシンのCPython2.7.3とPyPy1.8でスクリプトをテストしました。

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

これは、言語の実装に改善の余地がある可能性があることを示しています。おそらく、コンパイルされたHaskellは、対応するコードの解釈とコンパイルをせいぜい実行していません。Pythonで速度を探している場合は、必要に応じて、カバーするコードで許可されている場合は、pypyに切り替えることも検討してください。

于 2012-04-27T22:11:43.557 に答える
2

なんらかの理由で他の人が気づかなかった問題に気づきました。あなたのhaskellとpythonコードの両方がこれを持っています。(自動最適化で修正されているかどうか教えてください。最適化については何も知りません)。このために私はhaskellでデモンストレーションします。コードでは、次のように小さいリストと大きいリストを定義します。

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

これは悪いことです。なぜなら、xsの各要素をpと2回比較するからです。1回は小さい方のリストを取得するため、もう1回は大きい方のリストを取得するためです。これ(理論的には、タイミングをチェックしていません)により、ソートで2倍の比較が使用されます。これは災害です。代わりに、述語を使用してリストを2つのリストに分割する関数を作成する必要があります。

split f xs

と同等です

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

この種の関数を使用すると、リスト内の各要素を1回比較するだけで、タプルのどちら側に配置するかを知ることができます。
さて、それをしましょう:

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 / less部分を修正できないため、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 に答える
1

Pythonは、この種のことのために本当に最適化されています。Haskellはそうではないと思います。これは、いくつかの非常に良い答えを提供する同様の質問です。

于 2012-04-27T21:13:33.287 に答える