Python szybszy niż skompilowany Haskell?

Mam prosty skrypt napisany zarówno w Pythonie jak i Haskell. Odczytuje plik z 1,000,000 liczb całkowitych oddzielonych od nowej linii, przetwarza ten plik do listy liczb całkowitych, szybko sortuje go, a następnie zapisuje do innego posortowanego pliku. Ten plik ma taki sam format jak ten niesortowany. Proste.

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

A oto 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()
Bardzo proste. Teraz kompiluję Kod Haskell z
$ ghc -O2 --make quick.hs

I czas tych dwóch z:

$ time ./quick
$ time python qs.py

Wyniki:

Haskell:

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

Python:

real    0m9.888s
user    0m9.669s
sys 0m0.203s

Jak Python może być szybszy niż natywny kod Haskell?

Dzięki

EDIT :

  • wersja Pythona: 2.7.1
  • GHC wersja: 7.0.4
  • Mac OSX, 10.7.3
  • 2.4 GHz Intel Core i5

Lista wygenerowana przez

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

Więc wszystkie liczby są unikalne.

Author: Praveen, 2012-04-28

7 answers

W skrócie, nie używaj read. Zastąp read taką funkcją:

import Numeric

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

Dostaję całkiem niezły speedup:

~/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

Dla Zabawy, powyższe Wyniki zawierają wersję, która używa ByteString (a tym samym nie spełnia testu "ready for the 21st century", całkowicie ignorując problem kodowania plików) dla maksymalnej prędkości BARE-METAL. Ma również kilka innych różnic; na przykład, wysyła się do standardowej biblioteki funkcji sortowania. Pełny kod znajduje się poniżej.

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
 40
Author: Daniel Wagner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-27 22:10:42

Oryginalny Kod Haskell

Istnieją dwa problemy z wersją Haskell:

  • używasz string IO, który buduje połączone listy znaków
  • używasz nie-quicksort, który wygląda jak quicksort.
[9]}Ten program trwa 18.7 sekund, aby uruchomić na moim laptopie Intel Core2 2.5 GHz. (GHC 7.4-O2)

Daniel ' s ByteString Version

Jest to znacznie ulepszone, ale zauważ, że nadal wykorzystuje nieefektywne wbudowane sortowanie scalające.

Jego wersja zajmuje 8,1 sekundy (i nie obsługuje liczb ujemnych, ale to raczej nie problem dla tej eksploracji).

Uwaga

Stąd ta odpowiedź używa następujących pakietów: Vector, attoparsec, text i vector-algorithms. Zauważ również, że wersja kindalla z timsortem zajmuje 2,8 sekundy na moim komputerze (edit: i 2 sekundy z pypy).

Wersja Tekstowa

Wyrwałem wersję Daniela, przetłumaczyłem ją na Tekst (więc obsługuje różne kodowania) i dodano lepsze sortowanie za pomocą mutowalnego Vector w ST monadzie:

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')
To działa w 4 sekundy (i nie obsługuje negatywów)

Return To The Bytestring

Więc teraz wiemy, że możemy stworzyć bardziej ogólny program, który jest szybszy, co z szybką wersją tylko ASCii? Nie ma sprawy!

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')
To trwa 2,3 sekundy.

Tworzenie pliku testowego

Na wszelki wypadek ciekawe, mój plik testowy został wyprodukowany przez:

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)

Jeśli zastanawiasz się, dlaczego vsort nie jest zapakowany w jakąś łatwiejszą formę na Hackage... ja też.

 49
Author: Thomas M. DuBuisson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-22 19:37:06

[[4]}bardziej Pythonistą niż Haskellitem, ale wezmę dźgnięcie:

  1. Po prostu odczyt i zapisanie plików jest sporym obciążeniem w mierzonym środowisku uruchomieniowym, co prawdopodobnie jest podobne między tymi dwoma programami. Należy również uważać, aby rozgrzać pamięć podręczną dla obu programów.

  2. Większość czasu poświęcasz na tworzenie kopii list i fragmentów list. Operacje listy Pythona są mocno zoptymalizowane, będąc jedną z najczęściej używanych części język i składanie list są zwykle dość wydajne, spędzając większość czasu w C-landu wewnątrz interpretera Pythona. Nie ma zbyt wielu rzeczy, które są powolne w Pythonie, ale bardzo szybkie w językach statycznych, takich jak wyszukiwanie atrybutów na instancjach obiektów.

  3. Twoja implementacja Pythona wyrzuca liczby, które są równe pivotowi, więc pod koniec może sortować mniej elementów, dając mu oczywistą przewagę. (Jeżeli w zbiorze danych nie ma duplikatów sortujesz, to nie problem.) Naprawienie tego błędu prawdopodobnie wymaga wykonania kolejnej kopii większości listy w każdym wywołaniu qs(), Co spowolniłoby Pythona nieco bardziej.

  4. Nie wspominasz, jakiej wersji Pythona używasz. Jeśli używasz 2.x, prawdopodobnie Haskell mógłby pokonać Pythona po prostu przełączając się na Python 3.x. :-)

Nie jestem zbyt zaskoczony, że dwa języki są w zasadzie szyi i szyi tutaj (różnica 10% nie jest godna uwagi). Za pomocą C jako benchmark wydajności, Haskell traci pewną wydajność ze względu na swój leniwy charakter funkcjonalny, podczas gdy Python traci pewną wydajność z powodu bycia językiem interpretowanym. Przyzwoity mecz.

Ponieważ Daniel Wagner opublikował zoptymalizowaną wersję Haskell przy użyciu wbudowanego sort, Oto podobnie zoptymalizowana wersja Pythona przy użyciu 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 sekundy na mojej maszynie, a około 9 na oryginalny kod. Prawie wciąż szyja i szyja ze zoptymalizowanym Haskellem. Powód: to wydatki większość czasu spędza w bibliotekach programowanych w języku C. TimSort (rodzaj używany w Pythonie) jest bestią .
 30
Author: kindall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-27 23:34:20

To jest po fakcie, ale myślę, że większość kłopotów jest w piśmie Haskell. Poniższy moduł jest dość prymitywny - należy użyć budowniczych prawdopodobnie i na pewno uniknąć śmiesznego roundtrip poprzez ciąg do pokazywania - ale jest prosty i zrobił wyraźnie lepiej niż pypy z ulepszonym Pythonem kindalla i lepiej niż 2 i 4 sek Haskell Moduły gdzie indziej na tej stronie (to zaskoczyło mnie, jak bardzo używali list, więc zrobiłem kilka więcej obrotów z listy. crank.)

$ time aa.hs        real    0m0.709s
$ time pypy aa.py   real    0m1.818s
$ time python aa.py real    0m3.103s

Używam sortowania zalecanego dla wektorów z algorytmów wektorowych. Wykorzystanie danych.Wektor.Unboxed w jakiejś formie jest teraz standardowym, naiwnym sposobem robienia tego typu rzeczy . to nowe dane.List (for Int, Double, etc.) Wszystko poza {[2] } jest irytujące zarządzanie IO, które, jak sądzę, nadal może być znacznie ulepszone, w szczególności na końcu zapisu. Czytanie i sortowanie razem trwa około 0.2 sek, jak widać po zapytaniu go o wydrukowanie tego, co w kilku indeksach zamiast zapisu do pliku, więc dwa razy więcej czasu poświęca się na pisanie, niż w czymkolwiek innym. Jeśli pypy spędza większość swojego czasu używając timsort lub czegokolwiek innego, to wygląda na to, że samo sortowanie jest zdecydowanie lepsze w Haskell, i tak samo proste -- jeśli możesz po prostu dostać się w swoje ręce na darned vector...

Nie jestem pewien, dlaczego nie ma wygodnych funkcji do odczytu i zapisu wektorów rzeczy nieboskłonowych z naturalnych formatów -- gdyby były, to będą długie na trzy linie i unikną sznurków i będą znacznie szybsze, ale może po prostu ich nie widziałem.

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
Author: applicative,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-03 20:44:01

Aby śledzić @ kindall ciekawą odpowiedź, te czasy zależą zarówno od implementacji python / Haskell używasz, konfiguracji sprzętowej, na której uruchamiasz testy, i implementacji algorytmu prawo w obu językach.

Niemniej jednak możemy spróbować uzyskać kilka dobrych wskazówek na temat względnej wydajności jednej implementacji języka w porównaniu z inną, lub z jednego języka do innego języka. Z dobrze znanymi alogrytami jak qsort, jest to dobry początek.

Aby zilustrować porównanie Pythona / Pythona, przetestowałem Twój skrypt na CPython 2.7.3 i PyPy 1.8 na tej samej maszynie:

  • CPython: ~8s
  • PyPy: ~2,5 s

To pokazuje, że może być miejsce na ulepszenia w implementacji języka, być może skompilowany Haskell nie wykonuje co najwyżej interpretacji i kompilacji odpowiedniego kodu. Jeśli szukasz prędkości w Pythonie, rozważ również, aby przełączyć się na pypy w razie potrzeby i jeśli twój covering code pozwala ci to zrobić.

 2
Author: Boud,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-27 22:11:43

Zauważyłem pewien problem, którego inni nie zauważyli z jakiegoś powodu; zarówno Twój kod Haskell, jak i python mają to. (proszę mi powiedzieć, czy to jest naprawione w auto-optymalizacji, nic nie wiem o optymalizacji). w tym celu zademonstruję w haskell. w swoim kodzie definiujesz mniejsze i większe listy w ten sposób:

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

To jest złe, ponieważ porównujesz z P każdy element w xs dwa razy, raz za dostanie się na mniejszą listę i ponownie za dostanie się na większą listę. to (teoretycznie; nie sprawdzałem czasu) sprawia, że twój rodzaj używa dwa razy tyle porównań; to katastrofa. zamiast tego powinieneś utworzyć funkcję, która dzieli listę na dwie listy za pomocą predykatu, w taki sposób, że

split f xs

Jest równoważne

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

Używając tego rodzaju funkcji będziesz musiał porównać każdy element z listy raz , aby wiedzieć, po której stronie krotki ją umieścić.
ok, zróbmy to:

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)

Teraz wymieńmy generator mniejszy/większy z

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

Pełny kod:

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)

Z jakiegoś powodu nie mogę naprawić gettera / mniejszej części w klauzulach where, więc musiałem to naprawić w klauzulach let. ponadto, jeśli nie jest to tail-recursive daj mi znać i napraw go dla mnie (Jeszcze Nie wiem, jak tail-recorsive działa w pełni)

Teraz powinieneś zrobić to samo dla kodu Pythona. Nie znam Pythona, więc nie mogę tego zrobić za Ciebie.

EDIT: faktycznie zdarza się już taka funkcja w Data.Lista o nazwie partycja. zauważ, że dowodzi to potrzeby tego rodzaju funkcji, ponieważ w przeciwnym razie nie byłaby zdefiniowana. to skraca kod do:

quicksort :: Ord a => [a] -> [a]
quicksort []     = []
quicksort (p:xs) =
    let (lesser, greater) = partition (p>) xs
    in (quicksort lesser) ++ [p] ++ (quicksort greater)
 2
Author: user3246509,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-06 20:02:54

Python jest naprawdę zoptymalizowany do tego typu rzeczy. Podejrzewam, że Haskell nie jest. Oto podobne pytanie , które dostarcza bardzo dobrych odpowiedzi.

 1
Author: Community,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:17:23