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
. -
last
puede sustituirse por unasum
con 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 conwtf5
y posteriores, que no usanEnum
en absoluto. - Probablemente no sea un problema con
Num
,Int
oInteger
, porque puedo reproducir el comportamiento sin ellos enwtf14
ywtf16
.
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
let
flojo. - 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.