tuplas sobre recursivos recursivas potencia multiplos multiplicar funciones ejercicios ejemplos ciclos basico performance optimization haskell lazy-evaluation ghc

performance - sobre - multiplos en haskell



La pereza y la recursividad de la cola en Haskell, ¿por qué se cuelga? (3)

Tengo esta función bastante simple para calcular la media de los elementos de una gran lista, usando dos acumuladores para mantener la suma hasta el momento y el recuento hasta el momento:

mean = go 0 0 where go s l [] = s / fromIntegral l go s l (x:xs) = go (s+x) (l+1) xs main = do putStrLn (show (mean [0..10000000]))

Ahora, en un lenguaje estricto, esto sería recursivo de la cola, y no habría ningún problema. Sin embargo, como Haskell es flojo, mi Google me ha llevado a entender que (s + x) y (l + 1) se transmitirán por la recursión como thunks. Entonces todo esto se bloquea y arde:

Stack space overflow: current size 8388608 bytes.

Después de buscar en Google, ¡encontré seq y $! . Lo cual parece que no entiendo porque todos mis intentos de usarlos en este contexto resultaron inútiles, con mensajes de error que dicen algo acerca de los tipos infinitos.

Finalmente encontré -XBangPatterns , que lo resuelve todo cambiando la llamada recursiva:

go !s !l (x:xs) = go (s+x) (l+1) xs

Pero no estoy contento con esto, ya que -XBangPatterns es actualmente una extensión. Me gustaría saber cómo hacer que la evaluación sea estricta sin el uso de -XBangPatterns . (¡Y tal vez también aprenda algo!)

Para que entiendas mi falta de comprensión, esto es lo que probé (el único intento que compilé, es decir):

go s l (x:xs) = go (seq s (s+x)) (seq l (l+1)) xs

Por lo que pude entender, seq debería forzar aquí la evaluación del argumento syl, evitando así el problema causado por los thunks. Pero todavía tengo un desbordamiento de pila.


He escrito mucho sobre esto:

En primer lugar, sí, si desea solicitar una evaluación estricta de los acumuladores, use seq y quédese en Haskell 98:

mean = go 0 0 where go s l [] = s / fromIntegral l go s l (x:xs) = s `seq` l `seq` go (s+x) (l+1) xs main = print $ mean [0..10000000] *Main> main 5000000.0

En segundo lugar: el análisis de rigurosidad se activará si proporciona algunas anotaciones de tipo y compila con -O2:

mean :: [Double] -> Double mean = go 0 0 where go :: Double -> Int -> [Double] -> Double go s l [] = s / fromIntegral l go s l (x:xs) = go (s+x) (l+1) xs main = print $ mean [0..10000000] $ ghc -O2 --make A.hs [1 of 1] Compiling Main ( A.hs, A.o ) Linking A ... $ time ./A 5000000.0 ./A 0.46s user 0.01s system 99% cpu 0.470 total

Como ''Doble'' es un contenedor sobre el tipo atómico estricto Double #, con optimizaciones activadas y un tipo preciso, GHC ejecuta un análisis de rigor e infiere que la versión estricta estará bien.

import Data.Array.Vector main = print (mean (enumFromToFracU 1 10000000)) data Pair = Pair !Int !Double mean :: UArr Double -> Double mean xs = s / fromIntegral n where Pair n s = foldlU k (Pair 0 0) xs k (Pair n s) x = Pair (n+1) (s+x) $ ghc -O2 --make A.hs -funbox-strict-fields [1 of 1] Compiling Main ( A.hs, A.o ) Linking A ... $ time ./A 5000000.5 ./A 0.03s user 0.00s system 96% cpu 0.038 total

Como se describe en el capítulo anterior de RWH.


La función seq fuerza la evaluación del primer parámetro una vez que se llama a la función. Cuando pasa seq s (s+x) como parámetro, la función seq no se llama inmediatamente, porque no hay necesidad de evaluar el valor de ese parámetro. Desea que la llamada a seq se evalúe antes de la llamada recursiva, para que a su vez pueda forzar la evaluación de su parámetro.

Por lo general, esto se hace enlace esto:

go s l (x:xs) = s `seq` l `seq` go (s+x) (l+1) xs

Esta es una variación sintáctica de seq s (seq l (go (s+x) (l+1) xs)) . Aquí las llamadas a seq son las llamadas de función más externas en la expresión. Debido a la pereza de Haskell, esto hace que se evalúen primero: se llama seq con los parámetros no evaluados todavía s y seq l (go (s+x) (l+1) xs) , la evaluación de los parámetros se difiere hasta el punto en que alguien realmente intenta acceder a sus valores

Ahora, seq puede forzar la evaluación de su primer parámetro antes de devolver el resto de la expresión. Entonces el próximo paso en la evaluación sería la segunda seq . Si las llamadas a seq están enterradas en alguna parte de algún parámetro, es posible que no se ejecuten durante un tiempo prolongado, lo que frustra su propósito.

Con las posiciones modificadas de las seq , el programa se ejecuta correctamente, sin utilizar cantidades excesivas de memoria.

Otra solución al problema sería simplemente habilitar optimizaciones en GHC cuando se compila el programa ( -O o -O2 ). El optimizador reconoce la pereza prescindible y produce código que no asigna memoria innecesaria.


Tiene razón en su comprensión de que seq s (s+x) obliga a la evaluación de s . Pero no fuerza s+x , por lo tanto, todavía estás creando thunks.

¡Usando $! puede forzar la evaluación de la suma (dos veces, para ambos argumentos). Esto logra el mismo efecto que usar los patrones de explosión:

mean = go 0 0 where go s l [] = s / fromIntegral l go s l (x:xs) = ((go $! s+x) $! l+1) xs

El uso de $! la función traducirá el go $! (s+x) go $! (s+x) al equivalente de:

let y = s+x in seq y (go y)

Por lo tanto, y se fuerza primero a la forma normal de la cabeza débil , lo que significa que se aplica la función más externa. En el caso de y , la función más externa es + , por lo tanto, y se evalúa por completo a un número antes de pasar a go .

Ah, y probablemente recibiste el mensaje de error de tipo infinito porque no tenías el paréntesis en el lugar correcto. Obtuve el mismo error cuando primero escribí tu programa :-)

Porque los $! el operador es asociativo correcto, sin paréntesis go $! (s+x) $! (l+1) go $! (s+x) $! (l+1) go $! (s+x) $! (l+1) significa lo mismo que: go $! ((s+x) $! (l+1)) go $! ((s+x) $! (l+1)) , lo cual es obviamente incorrecto.