haskell - Fuga de espacio con uso redundante de seq en intérprete de GHC
space-leak (2)
Creo que @nm tiene razón. Nada está forzando el valor en la lista, por lo que el 1 + 1 + 1 + 1 + ... thunk eventualmente mata el espacio.
Prepararé una prueba rápida.
Escribo este código en el intérprete y la memoria se consume rápidamente:
last [1..10^7] `seq` ()
No puedo ver por qué esto necesita más que O (1) espacio. Si lo hago solo (¿cuál debería ser el mismo, porque Mostrar fuerza normal de cabeza débil, entonces la secuencia es redundante?):
last [1..10^7]
...funciona bien.
No puedo reproducir esta situación fuera del intérprete.
¿Que está pasando aqui?
Aquí hay algunos casos de prueba: http://hpaste.org/69234
Cosas a tener en cuenta:
- Al ejecutarme en el intérprete, cargo wtf.hs sin compilarlo, y escribo
wtf<n>en ghci. - Al compilar, hago
ghc --make wtf.hs && ./wtf. -
lastpuede sustituirse por unasumcon un acumulador estricto o una función que encuentre el elemento máximo en la lista, y la pérdida de espacio todavía ocurra - ¡No he visto este comportamiento cuando uso
$!en lugar deseq. - Intenté agregar un parámetro dummy
()porque pensé que quizás esto es un problema de CAF. No cambia nada. - Probablemente no sea un problema con las funciones en
Enum, porque puedo reproducir el comportamiento conwtf5y posteriores, que no usanEnumen absoluto. - Probablemente no sea un problema con
Num,IntoInteger, porque puedo reproducir el comportamiento sin ellos enwtf14ywtf16.
Intenté reproducir el problema con la aritmética de Peano para sacar las listas y los enteros de la ecuación (obtener el final de 10 ^ 9), pero me topé con otros problemas de pérdida de espacio / uso compartido que no entiendo al intentar implementar * .
Necesitamos ver la instancia de enumFromTo para Integer, y por último:
last [] = errorEmptyList "last"
last (x:xs) = last'' x xs
where last'' y [] = y
last'' _ (y:ys) = last'' y ys
Se define en GHC.Enum como:
enumFrom x = enumDeltaInteger x 1
enumFromThen x y = enumDeltaInteger x (y-x)
enumFromTo x lim = enumDeltaToInteger x 1 lim
dónde
enumDeltaInteger :: Integer -> Integer -> [Integer]
enumDeltaInteger x d = x `seq` (x : enumDeltaInteger (x+d) d)
-- strict accumulator, so
-- head (drop 1000000 [1 .. ]
-- works
y
enumDeltaToInteger :: Integer -> Integer -> Integer -> [Integer]
enumDeltaToInteger x delta lim
| delta >= 0 = up_list x delta lim
| otherwise = dn_list x delta lim
up_list :: Integer -> Integer -> Integer -> [Integer]
up_list x0 delta lim = go (x0 :: Integer)
where
go x | x > lim = []
| otherwise = x : go (x+delta)
last es totalmente perezoso, como se esperaba.
Para la clase Enum Integer, tenemos un acumulador estricto (explícitamente) para enumFrom . En el caso delimitado (por ejemplo, [1..n] ), llama a enumDeltaToInteger y luego a up_list , que utiliza un trabajador para desplegar una lista hasta que se alcanza su límite.
Pero up_list es estricto en x en el ayudante de go (ver la comparación con lim ).
Cuando se ejecuta en GHCi, nada de esto está optimizado, lo que produce llamadas ingenuas a enumFromTo, antes de devolver () .
let
it_ax6 :: ()
it_ax6 =
case last
@ GHC.Integer.Type.Integer
(GHC.Enum.enumFromTo
@ GHC.Integer.Type.Integer
GHC.Num.$fEnumInteger
(GHC.Integer.smallInteger 1)
(GHC.Real.^
@ GHC.Integer.Type.Integer
@ GHC.Integer.Type.Integer
GHC.Num.$fNumInteger
GHC.Real.$fIntegralInteger
(GHC.Integer.smallInteger 10)
(GHC.Integer.smallInteger 7)))
of _ -> GHC.Unit.()
in
GHC.Base.thenIO
@ ()
@ [()]
(System.IO.print @ () GHC.Show.$fShow() it_ax6)
(GHC.Base.returnIO
@ [()] (GHC.Types.: @ () it_ax6 (GHC.Types.[] @ ())))
Entonces, ¿por qué estamos reteniendo la lista en el caso de seq , y no en el caso normal? El caso normal se ejecuta bien en el espacio de construcción, confiando en la pereza de enumFromTo para Integer y para el last . El núcleo de GHCi para ese caso se ve así:
let {
it_aKj :: GHC.Integer.Type.Integer
[LclId,
Unf=Unf{Src=<vanilla>, TopLvl=False, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 170 0}]
it_aKj =
GHC.List.last
@ GHC.Integer.Type.Integer
(GHC.Enum.enumFromTo
@ GHC.Integer.Type.Integer
GHC.Num.$fEnumInteger
(GHC.Integer.smallInteger 1)
(GHC.Real.^
@ GHC.Integer.Type.Integer
@ GHC.Integer.Type.Integer
GHC.Num.$fNumInteger
GHC.Real.$fIntegralInteger
(GHC.Integer.smallInteger 10)
(GHC.Integer.smallInteger 7))) } in
GHC.Base.thenIO
@ ()
@ [()]
(System.IO.print
@ GHC.Integer.Type.Integer GHC.Num.$fShowInteger it_aKj)
(GHC.Base.returnIO
@ [()]
(GHC.Types.:
@ ()
(it_aKj
`cast` (UnsafeCo GHC.Integer.Type.Integer ()
:: GHC.Integer.Type.Integer ~ ()))
(GHC.Types.[] @ ())))
Así que estos son casi idénticos, con las diferencias siendo:
- en la versión
seq, lalast (enumFromTo ..)se fuerza dentro de uncase. - En la versión regular, es un
letflojo. - en la versión
seq, el valor se calcula y luego se descarta, lo que produce un()- nada mira el resultado - En el caso normal, se inspecciona y se muestra.
Lo que es extraño es que no hay nada mágico en:
let x = case last (enumFromTo 1 n) of _ -> ()
Eso lo hace retener valores.
Como vemos, la implementación de up_list es estricta en su acumulador (ya que se compara con lim , y la lista se despliega perezosamente, por lo que la last debería poder consumirla en un espacio constante). Escribir la expresión a mano confirma esto.
Al hacer un perfil de pila de la ejecución de ghci, se muestra la lista completa que se conserva:
lo que nos dice al menos que no es una cadena de thunks, sino que más bien, la lista completa se está construyendo estrictamente y se mantiene hasta que se descarta.
Entonces, el misterio es: ¿qué es lo que mantiene el argumento de la lista para last en ghci, y no en ghc?
Sospecho algún detalle interno (o sutil) de ghci ahora, creo que vale la pena un boleto de ghci.