python haskell quicksort

Python más rápido que compilado Haskell?



quicksort (7)

En resumen, no use read . Reemplace la read con una función como esta:

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

Tengo una aceleración bastante justa:

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

Solo por diversión, los resultados anteriores incluyen una versión que usa ByteString (y por lo tanto no ByteString prueba "lista para el siglo XXI" ignorando por completo el problema de las codificaciones de archivos) para la VELOCIDAD BARE-METAL MÁXIMA. También tiene algunas otras diferencias; por ejemplo, envía a la función de clasificación de la biblioteca estándar. El código completo está abajo.

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

Tengo un script simple escrito tanto en Python como en Haskell. Lee un archivo con 1,000,000 de enteros separados por nueva línea, analiza ese archivo en una lista de enteros, lo ordena rápidamente y luego lo escribe en un archivo diferente ordenado. Este archivo tiene el mismo formato que el sin clasificar. Sencillo.

Aquí está 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))

Y aquí está 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()

Muy simple. Ahora compilo el código Haskell con

$ ghc -O2 --make quick.hs

Y yo vez a esos dos con:

$ time ./quick $ time python qs.py

Resultados:

Haskell:

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

Pitón:

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

¿Cómo es posible que Python sea más rápido que el código nativo Haskell?

Gracias

EDITAR :

  • Versión de Python: 2.7.1
  • Versión de GHC: 7.0.4
  • Mac OSX, 10.7.3
  • Intel Core i5 de 2,4 GHz

Lista generada por

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

Entonces todos los números son únicos.


Esto es después del hecho, pero creo que la mayor parte del problema está en la escritura de Haskell. El siguiente módulo es bastante primitivo: uno debería usar constructores probablemente y sin duda evitar el ridículo viaje de ida y vuelta a través de String para mostrarlo, pero es simple e hizo mucho mejor que pypy con el mejorado Python de Kindall y mejor que los módulos Haskell de 2 y 4 segundos en esta página (me sorprendió lo mucho que usaban listas, así que hice un par de giros más de la manivela).

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

Estoy usando el tipo recomendado para vectores sin caja de algoritmos vectoriales. El uso de Data.Vector.Unboxed de alguna forma ahora es claramente la forma estándar e ingenua de hacer este tipo de cosas: es la nueva Data.List (para Int, Double, etc.) Todo menos el sort es irritante gestión de IO , que podría pensar que aún se mejora masivamente, en el extremo de la escritura en particular. La lectura y la ordenación juntas tardan unos 0,2 segundos, como se puede ver al solicitar que se imprima lo que hay en un montón de índices en lugar de escribir en un archivo, por lo que se gasta el doble de tiempo que en cualquier otra cosa. Si el padre está pasando la mayor parte del tiempo usando timsort o lo que sea, parece que la clasificación en sí misma es mucho mejor en Haskell, y lo más simple, si puede poner sus manos sobre el vector zurcido ...

No estoy seguro de por qué no hay funciones convenientes para leer y escribir vectores de elementos no empaquetados de formatos naturales; si los hubiera, esto sería de tres líneas y evitaría String y sería mucho más rápido, pero tal vez solo lo tengo. los has visto

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


Más un Pythonista que un Haskellite, pero daré una puñalada:

  1. Hay un poco de sobrecarga en el tiempo de ejecución medido simplemente leyendo y escribiendo los archivos, que es probablemente bastante similar entre los dos programas. Además, tenga cuidado de haber calentado el caché para ambos programas.

  2. La mayor parte de su tiempo lo dedica a hacer copias de listas y fragmentos de listas. Las operaciones de la lista de Python están muy optimizadas, siendo una de las partes más utilizadas del idioma, y ​​las listas de comprensión también suelen ser bastante efectivas, pasando gran parte de su tiempo en C-land dentro del intérprete de Python. No hay muchas cosas que se ralentizan en Python pero sí rápidas en los lenguajes estáticos, como las búsquedas de atributos en instancias de objetos.

  3. La implementación de Python descarta los números que son iguales al pivote, por lo que al final puede clasificar menos elementos, lo que le da una ventaja obvia. (Si no hay duplicados en el conjunto de datos que está ordenando, esto no es un problema). La reparación de este error probablemente requiera hacer otra copia de la mayor parte de la lista en cada llamada a qs() , lo que ralentizaría a Python. poco más.

  4. No mencionas qué versión de Python estás usando. Si está utilizando 2.x, probablemente consiga que Haskell venza a Python simplemente cambiando a Python 3.x. :-)

No estoy demasiado sorprendido de que los dos idiomas sean básicamente de cuello y cuello aquí (una diferencia del 10% no es digna de mención). Utilizando C como punto de referencia de rendimiento, Haskell pierde algo de rendimiento por su naturaleza funcional, mientras que Python pierde algo de rendimiento debido a que es un lenguaje interpretado. Una pareja decente.

Dado que Daniel Wagner publicó una versión optimizada de Haskell utilizando el tipo incorporado, aquí hay una versión de Python optimizada de manera similar que usa 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 segundos en mi máquina, frente a aproximadamente 9 para el código original. Todavía bastante al cuello con el Haskell optimizado. Motivo: pasa la mayor parte del tiempo en bibliotecas programadas en C. Además, TimSort (el género utilizado en Python) es una bestia.


Para dar seguimiento a la respuesta interesante de @kindall, esos tiempos dependen de la implementación de Python / Haskell que utilice, la configuración de hardware en la que ejecuta las pruebas y la implementación del algoritmo en ambos idiomas.

Sin embargo, podemos tratar de obtener algunos buenos indicios del rendimiento relativo de la implementación de un idioma en comparación con otro, o de un idioma a otro. Con alogritos conocidos como qsort, es un buen comienzo.

Para ilustrar una comparación python / python, acabo de probar su script en CPython 2.7.3 y PyPy 1.8 en la misma máquina:

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

Esto muestra que puede haber espacio para mejoras en la implementación del lenguaje, tal vez compilado. Haskell no está realizando a lo sumo la interpretación y compilación de su código correspondiente. Si está buscando velocidad en Python, considere cambiar a pypy si es necesario y si su código de cobertura le permite hacerlo.


Python está realmente optimizado para este tipo de cosas. Sospecho que Haskell no lo es. Aquí hay una pregunta similar que ofrece algunas muy buenas respuestas.


noté algún problema que los demás no notaron por alguna razón; tanto tu haskell como tu código python tienen esto. (dígame si está corregido en las optimizaciones automáticas, no sé nada sobre optimizaciones). para esto demostraré en Haskell. en su código usted define las listas cada vez menores como esta:

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

esto es malo, porque se compara con p cada elemento en xs dos veces, una para entrar en la lista menor y otra vez para entrar en la lista más grande. esto (teóricamente, no he comprobado el tiempo) hace que tu género use el doble de comparaciones; esto es un desastre. en su lugar, debe hacer una función que divida una lista en dos listas usando un predicado, de tal forma que

split f xs

es equivalente a

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

usando este tipo de función solo necesitarás comparar cada elemento en la lista una vez para saber en qué lado de la tupla ponerlo.
Está bien, hagámoslo:

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)

ahora vamos a reemplazar el generador menor / mayor con

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

código completo:

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)

por alguna razón, no puedo corregir la parte getter / lesser en las cláusulas where, así que tuve que corregirlo en las cláusulas let. Además, si no es recursivo de la cola, avíseme y solucione el problema (aún no sé cómo funciona la cola-rectora).

ahora deberías hacer lo mismo con el código python. No conozco a Python, así que no puedo hacerlo por ti.

EDITAR: realmente ya existe esa función en Data.List llamada partition. tenga en cuenta que esto demuestra la necesidad de este tipo de función porque de lo contrario no se definiría. esto reduce el código a:

quicksort :: Ord a => [a] -> [a] quicksort [] = [] quicksort (p:xs) = let (lesser, greater) = partition (p>) xs in (quicksort lesser) ++ [p] ++ (quicksort greater)


El código original de Haskell

Hay dos problemas con la versión de Haskell:

  • Está utilizando cadena IO, que crea listas de caracteres vinculadas
  • Está utilizando una conexión no rápida que se parece a la oferta rápida.

Este programa toma 18.7 segundos para ejecutarse en mi computadora portátil Intel Core2 2.5 GHz. (GHC 7.4 usando -O2)

Versión ByteString de Daniel

Esto es mucho mejor, pero note que todavía usa el tipo de combinación incorporada ineficiente.

Su versión demora 8.1 segundos (y no maneja números negativos, pero eso no es un problema para esta exploración).

Nota

A partir de ahora, esta respuesta utiliza los siguientes paquetes: Vector , attoparsec , text y vector-algorithms . También note que la versión de kindall que usa timsort toma 2.8 segundos en mi máquina (edición: y 2 segundos usando pypy).

Una versión de texto

Arranqué la versión de Daniel, la traduje a Texto (para que maneje varias codificaciones) y agregué una mejor clasificación usando un Vector mutable en una mónada de 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'')

Esto se ejecuta en 4 segundos (y tampoco maneja los negativos)

Volver a la Bytestring

Entonces, ahora sabemos que podemos hacer un programa más general que sea más rápido, ¿qué tal hacer que la versión de ASCii-only sea rápida? ¡No hay problema!

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

Esto funciona en 2.3 segundos.

Produciendo un archivo de prueba

En caso de que alguien tenga curiosidad, mi archivo de prueba fue producido por:

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)

Si te preguntas por qué vsort no está empaquetado de alguna forma más fácil en Hackage ... yo también.