¿Por qué este programa Haskell es mucho más lento que uno equivalente de Python?
performance io (3)
Como parte de un desafío de programación, necesito leer, desde stdin, una secuencia de enteros separados por espacios ( en una sola línea ) e imprimir la suma de esos enteros a stdout. La secuencia en cuestión puede contener hasta 10,000,000 de enteros.
Tengo dos soluciones para esto: una escrita en Haskell ( foo.hs
), y otra, equivalente, escrita en Python 2 ( foo.py
). Lamentablemente, el programa (compilado) Haskell es consistentemente más lento que el programa Python, y no puedo explicar la discrepancia en el rendimiento entre los dos programas; vea la sección de referencia a continuación. En todo caso, hubiera esperado que Haskell tuviera la ventaja ...
¿Qué estoy haciendo mal? ¿Cómo puedo explicar esta discrepancia? ¿Hay alguna manera fácil de acelerar mi código Haskell?
(Para información, estoy usando un Macbook Pro de mediados de 2010 con 8 Gb de RAM, GHC 7.8.4 y Python 2.7.9).
foo.hs
main = print . sum =<< getIntList
getIntList :: IO [Int]
getIntList = fmap (map read . words) getLine
(compilado con ghc -O2 foo.hs
)
foo.py
ns = map(int, raw_input().split())
print sum(ns)
Punto de referencia
A continuación, test.txt
consta de una única línea de 10 millones de enteros separados por espacios.
# Haskell
$ time ./foo < test.txt
1679257
real 0m36.704s
user 0m35.932s
sys 0m0.632s
# Python
$ time python foo.py < test.txt
1679257
real 0m7.916s
user 0m7.756s
sys 0m0.151s
Me atrevería a adivinar que una gran parte de tu problema son las words
. Cuando map read . words
map read . words
, lo que en realidad estás haciendo es esto:
- Escanee la entrada buscando un espacio, construyendo una lista de espacios no a medida que avanza. Hay muchos tipos diferentes de espacios, y la comprobación de cualquier carácter que no sea un tipo común de espacio implica adicionalmente una llamada externa a una función C (lenta). Estoy planeando arreglar esto alguna vez, pero todavía no he llegado a eso, e incluso así estarás construyendo y tirando listas sin ninguna buena razón, y buscando espacios cuando realmente solo quieres verificar dígitos
- Lea la lista de caracteres acumulados para tratar de hacer un número de ellos. Producir el número. La lista acumulada ahora se convierte en basura.
- Regresa al paso 1.
Esta es una manera bastante ridícula de proceder. Creo que incluso puedes hacerlo mejor usando algo horrible como las reads
, pero tendría más sentido usar algo como ReadP . También puedes probar tipos más geniales de cosas como el análisis basado en secuencias; No sé si eso ayudará mucho o no.
read
es lento Para el análisis masivo, use bytestring
o text
primitives, o attoparsec
.
Hice algunas evaluaciones comparativas. Su versión original se ejecutó en 23,9 segundos en mi computadora. La versión siguiente funciona en 0.35 segundos:
import qualified Data.ByteString.Char8 as B
import Control.Applicative
import Data.Maybe
import Data.List
import Data.Char
main = print . sum =<< getIntList
getIntList :: IO [Int]
getIntList =
map (fst . fromJust . B.readInt) . B.words <$> B.readFile "test.txt"
Al especializar el analizador en su archivo test.txt
, pude obtener el tiempo de ejecución en 0.26 segundos:
getIntList :: IO [Int]
getIntList =
unfoldr (B.readInt . B.dropWhile (=='' '')) <$> B.readFile "test.txt"
Leer es lento
La lectura rápida, a partir de esta respuesta , le reducirá a 5,5 segundos.
import Numeric
fastRead :: String -> Int
fastRead s = case readDec s of [(n, "")] -> n
Las cadenas son listas vinculadas
En Haskell, el tipo de String
es una lista enlazada. Usar una representación empaquetada ( bytestring
si realmente solo quieres ascii pero Text
también es muy rápido y admite unicode). Como se muestra en esta respuesta , el rendimiento debería ser el cuello y el cuello.