Enlazar` len = longitud xs `y luego calcular` len ' hace que GHC consuma mucha RAM


Encontré algo extraño sobre GHCi y las listas.

Este comando tarda algún tiempo en ejecutarse y solo devuelve la respuesta correcta.

ghci> length [1..10^8]
100000000

Sin embargo, vincular esto a una variable y ejecutar hace que GHC consuma aproximadamente 5 GiB de RAM sin liberar hasta que la sesión de GHCi haya terminado. Escribiendo :quit después de consumir 3 GiB más antes de salir realmente.

ghci> len = length [1..10^8]
ghci> len
-- Consumes 5 GiB
100000000
ghci> :quit
-- Consumes 3 GiB
-- Exits

¿Es esto normal? ¿Cuál es la diferencia entre los comandos?

La versión de GHC es 8.2.2.

Author: Alex, 2018-02-10

1 answers

Actualización: La optimización realizada por -O0 es un poco diferente de lo que entendí al principio. Además, se agregó una nota sobre la presentación de un nuevo error Trac.

Puedo reproducir esto en GHC 8.2.2. Evaluar directamente la expresión (o usar let para vincularla a una variable y luego evaluarla) ambos se completan 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 mucha memoria que no se libera hasta la sesión final.

Tenga en cuenta que esto es específico de GHCi y el modo interactivo in en un programa Haskell compilado real, no habría ningún problema. Compilar lo siguiente se ejecutará rápidamente y no consumirá exceso de memoria:

len = length [1..10^8]
main = print len

Para entender lo que está pasando, debes entender que Haskell es capaz de realizar dos optimizaciones potenciales de este código:

  1. Puede crear explícitamente una lista perezosa y comenzar a calcular su longitud, pero determinar que una vez que el comienzo de la lista ha sido contado, esos elementos no serán necesarios de nuevo lo que les permite ser inmediatamente basura recogida.
  2. Puede determinar que no es necesario crear ninguna lista, y a través de un proceso conocido como "fusión de listas", crear código compilado que cuente directamente desde 1 hasta 10^8 sin intentar poner 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 ejecute 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 alrededor de 1.6 gigabytes de montón, pero esto es en realidad para almacenar los valores individuales Integer a medida que se incrementan. (Dado que los valores en Haskell son inmutables, se debe asignar un nuevo Integer para cada incremento.) Si fuerzas el tipo a ser Int:

len = length [(1::Int)..10^8]

Entonces el programa asignar solo unos pocos kilobytes de montón, y se puede ver que realmente no hay una lista que se asigna.

Resulta que cuando este código se compila sin optimizaciones (-O0), GHC solo realiza la optimización #1 (como señala @Carl), pero logra hacer un buen trabajo, tanto que a pesar de que las estadísticas de GHC muestran mucha asignación de montones, el programa todavía se ejecuta bastante rápido con una huella de memoria muy pequeña.

Sin embargo, cuando este código es compilado a código de bytes en GHCi, no solo se utiliza la optimización #1, sino que GHC no hace un buen trabajo de recolección de basura de la lista. Se genera una enorme lista de varios gigabytes, y el comienzo es la basura recolectada casi tan rápido como se genera. El uso de 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 en realidad asigna un gigabyte 7.2 lista; afortunadamente, se puede tirar casi tan rápido como se genera, por lo que la memoria en uso por el proceso GHCi después de este cálculo seguirá siendo razonablemente modesta.

Verás que:

> let len = length [1..10^8]
> len

Y:

> len = length [1..10^8]
> len

Mastique exactamente la misma enorme cantidad de memoria (aproximadamente 7.2 gigas).

La diferencia es que, por alguna razón, la versión let permite que la lista sea recogida como basura a medida que se cuenta, y la versión no-let no lo hace.

Al final, esto es casi seguro un error de GHCi. Podría 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 es uno nuevo. Decidí archivarlo como #14789, así que tal vez alguien le eche un vistazo.

 18
Author: K. A. Buhr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-10 21:38:58