web-dev-qa-db-ja.com

Pythonコンパイルされた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

そして、私はこれら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()

したがって、すべての数値は一意です。

44
Honza Pokorny

つまり、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

面白くするために、上記の結果には、ULTIMATE BARE-METAL SPEEDのByteStringを使用するバージョンが含まれています(したがって、ファイルエンコーディングの問題を完全に無視することにより、「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
41
Daniel Wagner

元のHaskellコード

Haskellバージョンには2つの問題があります。

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

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

ダニエルのByteStringバージョン

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

彼のバージョンは8.1秒かかります(負の数は処理しませんが、この探索では問題ではありません)。

ここからは、この回答は次のパッケージを使用します:Vectorattoparsectextおよびvector-algorithms。また、timsortを使用したkindallのバージョンは、私のマシンで2.8秒かかります(編集:pypyを使用して2秒)。

テキスト版

Danielのバージョンを取り除き、Textに変換して(さまざまなエンコーディングを処理できるように)、STモナドで可変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の簡単な形式でパッケージ化されていないのか疑問に思っているのなら、私もそうです。

50

HaskelliteよりもPythonistaですが、私は刺します:

  1. 測定されたランタイムには、ファイルの読み取りと書き込みだけでかなりのオーバーヘッドがあります。これは、おそらく2つのプログラム間でかなり似ています。また、両方のプログラムのキャッシュをウォームアップしたことにも注意してください。

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

  3. あなたのPython実装はピボットに等しい数を捨てるので、結局それはより少ないアイテムをソートするかもしれません、それはそれに明らかな利点を与えます。(もしデータセットに重複がないなら、 '並べ替え中、これは問題ではありません。)このバグを修正するには、おそらくqs()を呼び出すたびにリストのほとんどのコピーをもう1つ作成する必要があります。これにより、Pythonもう少し下に。

  4. 使用しているPythonのバージョンについては言及していません。2.xを使用している場合は、HaskellにPython Python 3.x. :-)に切り替えて

私は2つの言語が基本的にここで首をかしげていることに驚くことはありません(10%の違いは注目に値しません)。 Cをパフォーマンスベンチマークとして使用すると、Haskellは遅延機能の性質上、ある程度のパフォーマンスを失いますが、Pythonは、インタープリター型言語であるため、ある程度のパフォーマンスを失います。

Daniel Wagnerが組み込みのsortを使用して最適化されたHaskellバージョンを投稿したため、同様に最適化された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で使用される種類)はbeast。です。

30
kindall

これは事実の後ですが、問題のほとんどはハスケルの執筆にあると思います。次のモジュールはかなり原始的です-ビルダーを使用して、おそらく間違いなくString経由のばかげたラウンドトリップを回避する必要があります-しかし、シンプルで、pypyよりも明らかに優れており、kindallの改良されたpythonと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秒かかります。そのため、他の場合と比べて、2倍の時間が書き込みに費やされます。 pypyがtimsortなどを使用してほとんどの時間を費やしている場合、Haskellではソート自体が確かに大幅に改善されているように見えますが、同じように単純です-とんでもないベクトルを手に入れることができれば...

自然な形式からボックス化されていないもののベクトルを読み書きするための便利な関数がない理由はわかりません-ある場合、これは3行の長さで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
_
9
applicative

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

それでも、ある言語の実装と別の言語の実装との相対的なパフォーマンス、またはある言語から別の言語への相対的なパフォーマンスについて、いくつかの良いヒントを得ようとすることができます。 qsortのようなよく知られたalogrithmsで、それは良い始まりです。

Python/pythonの比較を示すために、同じマシンでCPython 2.7.3とPyPy 1.8でスクリプトをテストしました。

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

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

2
Boud

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

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

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

split f xs

に相当

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

この種類の関数を使用すると、タプルのどちら側に配置するかを知るために、リスト内の各要素onceを比較するだけで済みます。
よし、やってみましょう:

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句のゲッター/レッサー部分を修正できないため、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)
2
user3246509

Pythonは、この種のことのために本当に最適化されています。 Haskellはそうではないと思います。これが 同様の質問 であり、非常に優れた回答を提供します。

1
user1356386