list - El enlace `len=length xs` y luego el cálculo de` len` hace que GHC consuma mucha RAM
haskell ghci (1)
Encontré algo extraño sobre GHCi y las listas.
Este comando tarda algún tiempo en ejecutarse y simplemente devuelve la respuesta correcta.
ghci> length [1..10^8]
100000000
Sin embargo, vincular esto con una variable y ejecutar hace que GHC consuma aproximadamente 5 GiB de RAM sin liberar hasta que finalice la sesión de GHCi. Escribiendo :quit
después de que consume 3 GiB más antes de salir.
ghci> len = length [1..10^8]
ghci> len
-- Consumes 5 GiB
100000000
ghci> :quit
-- Consumes 3 GiB
-- Exits
¿Esto es normal? ¿Cuál es la diferencia entre los comandos?
La versión de GHC es 8.2.2.
Actualización: la optimización realizada por -O0
es un poco diferente de la que entendí por primera vez. Además, agregó una nota sobre la presentación de un nuevo error Trac.
Puedo reproducir esto en GHC 8.2.2. La evaluación directa de la expresión (o el uso de let
para enlazarla a una variable y luego evaluarla) se completa rápidamente:
Prelude> length [1..10^8]
10000000 -- pretty fast
Prelude> let len = length [1..10^8]
Prelude> len
10000000 -- pretty fast
Prelude>
Sin embargo, usando la sintaxis let
-free:
Prelude> len = length [1..10^8]
Prelude> len
10000000
Prelude>
toma más tiempo y asigna una gran cantidad de memoria que no se libera hasta que finaliza la sesión.
Tenga en cuenta que esto es específico de GHCi y el modo interactivo: en un programa Haskell real y compilado, no habría ningún problema. La compilación de lo siguiente se ejecutará rápidamente y no consumirá el exceso de memoria:
len = length [1..10^8]
main = print len
Para comprender lo que sucede, debe comprender que Haskell es capaz de realizar dos optimizaciones potenciales de este código:
- Puede crear explícitamente una lista lenta y comenzar a calcular su longitud, pero determinar que una vez que se haya contado el comienzo de la lista, esos elementos no serán necesarios nuevamente, lo que permitirá que se recojan de inmediato.
- Puede determinar que no es necesario crear una lista y, a través de un proceso conocido como "fusión de lista", cree un código compilado que cuente directamente desde 1 hasta 10 ^ 8 sin intentar colocar esos números en ningún tipo de estructura de datos.
Cuando este código se compila con optimizaciones ( -O1
o -O2
), GHC realizará la optimización # 2. La versión compilada se ejecutará rápidamente en una pequeña cantidad de memoria constante (un par de megabytes residentes para el tiempo de ejecución). Si ejecuta esto con:
$ time ./Length +RTS -s
para recopilar estadísticas, encontrará que GHC todavía está asignando aproximadamente 1.6 gigabytes de almacenamiento dinámico, pero esto en realidad es para almacenar los valores de los Integer
individuales a medida que se incrementan. (Dado que los valores en Haskell son inmutables, se debe asignar un nuevo Integer
para cada incremento). Si obliga a que el tipo sea Int
:
len = length [(1::Int)..10^8]
entonces el programa asignará solo unos pocos kilobytes de pila, y podrá ver que realmente no hay una lista asignada.
Resulta que cuando este código se compila sin optimizaciones ( -O0
), GHC solo realiza la optimización # 1 (como lo señala @Carl), pero logra hacer un muy buen trabajo, tanto que incluso aunque Las estadísticas de GHC muestran una gran cantidad de asignación de almacenamiento dinámico, el programa aún se ejecuta con bastante rapidez con una huella de memoria muy pequeña.
Sin embargo, cuando este código se compila a byte-code en GHCi, no solo se usa la optimización # 1, sino que GHC no hace un trabajo tan bueno como recolectar la lista. Se genera una enorme lista de varios gigabytes, y el principio es la recolección de basura casi tan rápido como se genera. El uso de la memoria termina siendo bastante grande, pero al menos es relativamente constante.
Puedes ver esto activando las estadísticas de tiempo / memoria:
> :set +s
> length [1..10^8]
100000000
(1.54 secs, 7,200,156,128 bytes)
>
Esto significa que este código realmente asigna una lista de 7.2 gigabytes; afortunadamente, puede desecharse casi tan rápido como se genera, por lo que la memoria en uso por el proceso GHCi después de este cálculo todavía será razonablemente modesta.
Verás que:
> let len = length [1..10^8]
> len
y:
> len = length [1..10^8]
> len
mastique exactamente la misma cantidad de memoria (aproximadamente 7.2 gigas).
La diferencia es que, por alguna razón, la versión para let
permite que la lista se recoja como se cuenta, y la versión sin let
no lo hace.
Al final, esto es casi seguro que es un error de GHCi. Puede estar relacionado con uno de los errores de fuga de espacio existentes que se han reportado (por ejemplo, Trac #12848 o #14336 ), o tal vez sea uno nuevo. Decidí archivarlo como #14789 , así que tal vez alguien lo eche un vistazo.