16

Me tropecé con Haskell y FP y quedé aturdido por las posibilidades. Y el viejo nerd matemático dentro de mí no tuvo problemas para escribir código ingenuo para fines realmente útiles. Sin embargo, a pesar de todas las lecturas, todavía me cuesta entender cómo no dar algunos sorprendentes cuellos de botella en el rendimiento.Dificultad para comprender el comportamiento de asignación de memoria de Haskell

Así que escribo fragmentos de código muy cortos con implementaciones ingenuas y luego trato de hacer pequeños cambios para ver cómo responde el rendimiento. Y aquí hay un ejemplo que realmente no puedo entender: escribí esta función que encuentra una solución para Josephus problem, con propósito con una implementación ingenua de listas.

m = 3 
n = 3000 
main = putStr $ "Soldier #" ++ (show $ whosLeft [1..n]) ++ " survived...\n" 

whosLeft [lucky] = lucky 
whosLeft soldiers = whosLeft $ take (length soldiers -1) $ drop m $ cycle soldiers 

Este último funciona en 190 ms, con una productividad del 63% según el RTS.

Luego, lo primero que quería probar era eliminar el (soldado de longitud -1) y reemplazarlo por un entero decreciente.

¡El tiempo de ejecución aumentó hasta 900 ms y la productividad bajó al 16%, y usa 47 veces más memoria que el código más simple de arriba! Así que agregué una evaluación estricta, forcé el tipo Int, probé cosas como eliminar las variables globales y otras, pero no sirvió de mucho. Y simplemente no puedo entender esta desaceleración.

m = 3::Int 
n = 3000::Int 
main = putStr $ "Soldier #" ++ (show $ whosLeft n [1..n]) ++ " survived...\n" 

whosLeft 1 [lucky] = lucky 
whosLeft n' soldiers = n' `seq` left `seq` whosLeft (n'-1) left 
       where left = take (n'-1) $ drop m $ cycle soldiers 

He revisado los artículos y las publicaciones relacionadas con el rendimiento, pero parece que no tengo una pista al respecto. Aún siendo un novato de Haskell, me falta algo grande ... ¿Cómo puede este parámetro añadido (computación pre-masticada ...) reducir la velocidad tanto?

PD: Sé que, si Josefo realmente había estado con 3000 soldados, no habrían necesitado al suicidio ...

+1

No hay necesidad de buscar ', whosLeft ya es estricto en n'. Pero deberías compilar con optimización. – augustss

Respuesta

9

La primera solución obliga a toda la columna vertebral de la lista soldados tomando su longitud. La segunda solución solo fuerza (usando seq) la cabeza de la lista de soldados. Reemplace el left entre el seq s con un length left y recuperará su rendimiento.

+0

Increíble. Así que me había perdido completamente el punto, y fue simple después de todo: D Muchas gracias sclv. Es un truco interesante para saber. –

+0

¿Es este el tipo de cosa para la que es útil el paquete [deepseq] (http://hackage.haskell.org/package/deepseq)? –

+1

@Antal: No me gusta usar deepseq, porque es un instrumento contundente. En este caso, por ejemplo, solo necesitas forzar el lomo de la lista. Deepseq también fuerza los valores. Es relativamente barato, ya que en esencia ya están forzados, pero hay un costo menor, y si lo multiplica por la longitud de la lista y el número de llamadas recursivas, se suma. Deepseq está bien si tienes prisa, pero creo que es mejor cuando sea posible mezclar la pereza y el rigor de una manera óptima, forzando donde tenga sentido operacional en lugar de indiscriminadamente. – sclv

Cuestiones relacionadas