Estou curioso para saber por que as implementações de Haskell usam um GC.
Não consigo pensar em um caso em que o GC seria necessário em uma linguagem pura. É apenas uma otimização para reduzir as cópias ou é realmente necessário?
Estou procurando por exemplo de código que vazaria se um GC não estivesse presente.
haskell
garbage-collection
Pubby
fonte
fonte
Respostas:
Como outros já observaram, Haskell requer automático , dinâmico gerenciamento memória: o gerenciamento automático de memória é necessário porque o gerenciamento manual de memória não é seguro; o gerenciamento de memória dinâmica é necessário porque, para alguns programas, o tempo de vida de um objeto só pode ser determinado em tempo de execução.
Por exemplo, considere o seguinte programa:
Neste programa, a lista
[1..1000]
deve ser mantida na memória até que o usuário digite "limpar"; portanto, o tempo de vida disso deve ser determinado dinamicamente e é por isso que o gerenciamento de memória dinâmico é necessário.Portanto, nesse sentido, a alocação de memória dinâmica automatizada é necessária e, na prática, isso significa: sim , Haskell requer um coletor de lixo, já que a coleta de lixo é o gerenciador automático de memória dinâmica de mais alto desempenho.
Contudo...
Embora um coletor de lixo seja necessário, podemos tentar encontrar alguns casos especiais em que o compilador pode usar um esquema de gerenciamento de memória mais barato do que a coleta de lixo. Por exemplo, dado
podemos esperar que o compilador detecte o que
x2
pode ser desalocado com segurança quandof
retornar (em vez de esperar que o coletor de lixo seja desalocadox2
). Essencialmente, estamos pedindo que o compilador execute uma análise de escape para converter as alocações em heap coletado pelo lixo para alocações na pilha sempre que possível.Não é muito razoável perguntar: o compilador haskell jhc faz isso, embora o GHC não. Simon Marlow diz que o coletor de lixo geracional do GHC torna a análise de fuga quase desnecessária.
A jhc realmente usa uma forma sofisticada de análise de escape conhecida como inferência de região . Considerar
Nesse caso, uma análise de escape simplista concluiria que
x2
escapa def
(porque é retornado na tupla) e, portanto,x2
deve ser alocado no heap coletado pelo lixo. A inferência de região, por outro lado, é capaz de detectar quex2
pode ser desalocada quandog
retorna; a ideia aqui é quex2
deva ser alocado nag
região de e nãof
na região de.Além de Haskell
Embora a inferência de região seja útil em certos casos, conforme discutido acima, parece ser difícil conciliar efetivamente com a avaliação preguiçosa (veja os comentários de Edward Kmett e Simon Peyton Jones ). Por exemplo, considere
Alguém pode ficar tentado a alocar a lista
[1..n]
na pilha e desalocá-la após of
retorno, mas isso seria catastrófico: mudariaf
de usar memória O (1) (sob coleta de lixo) para memória O (n).Um extenso trabalho foi feito na década de 1990 e no início de 2000 na inferência de região para a linguagem funcional estrita ML. Mads Tofte, Lars Birkedal, Martin Elsman e Niels Hallenberg escreveram uma retrospectiva bastante legível sobre seu trabalho em inferência de região, grande parte da qual eles integraram ao compilador MLKit . Eles experimentaram com gerenciamento de memória puramente baseado em região (ou seja, sem coletor de lixo), bem como gerenciamento de memória híbrido baseado em região / coletado por lixo, e relataram que seus programas de teste rodaram "entre 10 vezes mais rápido e 4 vezes mais lento" do que o lixo puro- versões coletadas.
fonte
Nothing
) Para a chamada recursivaloop
e desalocar a antiga - sem tempo de vida desconhecido. Claro que ninguém quer uma implementação sem compartilhamento de Haskell, porque é terrivelmente lento para grandes estruturas de dados.Vamos dar um exemplo trivial. Dado isso
você precisa alocar o par em
(x, y)
algum lugar antes de chamarf
. Quando você pode desalocar esse par? Você não tem ideia. Ele não pode ser desalocado quandof
retorna, porquef
pode ter colocado o par em uma estrutura de dados (por exemplo,f p = [p]
), então a vida útil do par pode ter que ser mais longa do que o retorno def
. Agora, digamos que o par tenha sido colocado em uma lista, quem desmonta a lista pode desalocar o par? Não, porque o par pode ser compartilhado (por exemplo,let p = (x, y) in (f p, p)
). Portanto, é realmente difícil dizer quando o par pode ser desalocado.O mesmo é válido para quase todas as alocações em Haskell. Dito isso, é possível ter uma análise (análise de região) que fornece um limite superior para o tempo de vida. Isso funciona razoavelmente bem em linguagens restritas, mas nem tanto em linguagens preguiçosas (linguagens preguiçosas tendem a fazer muito mais mutações do que linguagens estritas na implementação).
Então, eu gostaria de inverter a questão. Por que você acha que Haskell não precisa de GC. Como você sugere que a alocação de memória seja feita?
fonte
Sua intuição de que isso tem algo a ver com pureza tem alguma verdade.
Haskell é considerado puro em parte porque os efeitos colaterais das funções são contabilizados na assinatura do tipo. Portanto, se uma função tem o efeito colateral de imprimir algo, deve haver um
IO
algum lugar em seu tipo de retorno.Mas há uma função que é usada implicitamente em todos os lugares em Haskell e cuja assinatura de tipo não é responsável, o que é, em certo sentido, um efeito colateral. Ou seja, a função que copia alguns dados e lhe dá duas versões de volta. Nos bastidores, isso pode funcionar literalmente, duplicando os dados na memória, ou 'virtualmente', aumentando uma dívida que precisa ser paga mais tarde.
É possível projetar linguagens com sistemas de tipos ainda mais restritivos (puramente "lineares") que não permitem a função de cópia. Do ponto de vista de um programador em tal linguagem, Haskell parece um pouco impuro.
Na verdade, Clean , um parente de Haskell, tem tipos lineares (mais estritamente: únicos) e isso pode dar uma ideia de como seria não permitir a cópia. Mas o Clean ainda permite a cópia de tipos "não exclusivos".
Há muitas pesquisas nesta área e, se você pesquisar no Google, encontrará exemplos de código linear puro que não requer coleta de lixo. Você encontrará todos os tipos de sistemas de tipo que podem sinalizar para o compilador qual memória pode ser usada, permitindo que o compilador elimine parte do GC.
Em certo sentido, os algoritmos quânticos também são puramente lineares. Cada operação é reversível e, portanto, nenhum dado pode ser criado, copiado ou destruído. (Eles também são lineares no sentido matemático usual.)
Também é interessante comparar com Forth (ou outras linguagens baseadas em pilha), que têm operações DUP explícitas que deixam claro quando a duplicação está acontecendo.
Outra maneira (mais abstrata) de pensar sobre isso é observar que Haskell é construído a partir do cálculo lambda simplesmente digitado, que se baseia na teoria das categorias fechadas cartesianas e que tais categorias vêm equipadas com uma função diagonal
diag :: X -> (X, X)
. Uma linguagem baseada em outra classe de categoria pode não ter tal coisa.Mas, em geral, a programação puramente linear é muito difícil para ser útil, então optamos pelo GC.
fonte
As técnicas de implementação padrão aplicadas a Haskell realmente requerem um GC mais do que a maioria das outras linguagens, uma vez que eles nunca alteram valores anteriores, em vez de criar novos valores modificados com base nos anteriores. Como isso significa que o programa está constantemente alocando e usando mais memória, um grande número de valores será descartado com o passar do tempo.
É por isso que os programas GHC tendem a ter números totais de alocação tão altos (de gigabytes a terabytes): eles estão constantemente alocando memória, e é apenas graças ao GC eficiente que eles a recuperam antes de acabar.
fonte
Se uma linguagem (qualquer linguagem) permite que você aloque objetos dinamicamente, então existem três maneiras práticas de lidar com o gerenciamento de memória:
O idioma só pode permitir que você aloque memória na pilha ou na inicialização. Mas essas restrições limitam severamente os tipos de cálculos que um programa pode realizar. (Na prática. Em teoria, você pode emular estruturas de dados dinâmicas em (digamos) Fortran, representando-as em um grande array. É HORRÍVEL ... e não relevante para esta discussão.)
A linguagem pode fornecer um mecanismo
free
ou explícitodispose
. Mas isso depende do programador para acertar. Qualquer erro no gerenciamento de armazenamento pode resultar em vazamento de memória ... ou pior.A linguagem (ou mais estritamente, a implementação da linguagem) pode fornecer um gerenciador de armazenamento automático para o armazenamento alocado dinamicamente; ou seja, alguma forma de coletor de lixo.
A única outra opção é nunca recuperar o armazenamento alocado dinamicamente. Esta não é uma solução prática, exceto para pequenos programas que executam pequenos cálculos.
Aplicando isso a Haskell, a linguagem não tem a limitação de 1. e não há operação de desalocação manual de acordo com 2. Portanto, para ser utilizável para coisas não triviais, uma implementação de Haskell precisa incluir um coletor de lixo .
Presumivelmente, você se refere a uma linguagem funcional pura.
A resposta é que um GC é necessário sob o capô para recuperar os objetos heap que a linguagem DEVE criar. Por exemplo.
Uma função pura precisa criar objetos heap porque, em alguns casos, ela precisa retorná-los. Isso significa que eles não podem ser alocados na pilha.
O fato de que pode haver ciclos (resultante de um,
let rec
por exemplo) significa que uma abordagem de contagem de referência não funcionará para objetos de heap.Depois, há encerramentos de função ... que também não podem ser alocados na pilha porque têm um tempo de vida que é (normalmente) independente do quadro de pilha em que foram criados.
Praticamente qualquer exemplo que envolvesse fechamentos ou estruturas de dados em forma de gráfico vazaria nessas condições.
fonte
Um coletor de lixo nunca é necessário, desde que você tenha memória suficiente. No entanto, na realidade, não temos memória infinita e, portanto, precisamos de algum método para recuperar a memória que não é mais necessária. Em linguagens impuras como C, você pode declarar explicitamente que acabou com alguma memória para liberá-la - mas esta é uma operação de mutação (a memória que você acabou de liberar não é mais segura para ler), então você não pode usar essa abordagem em uma linguagem pura. Portanto, é de alguma forma analisar estaticamente onde você pode liberar a memória (provavelmente impossível no caso geral), vazar memória como uma peneira (funciona muito bem até acabar) ou usar um GC.
fonte
GC é "obrigatório" em linguagens FP puras. Por quê? Operações alocadas e gratuitas são impuras! E a segunda razão é que estruturas de dados recursivas imutáveis precisam de GC para existir porque backlinking cria estruturas abstrusas e impossíveis de manutenção para a mente humana. Claro, backlinking é uma bênção, porque copiar estruturas que o utilizam é muito barato.
De qualquer forma, se você não acredita em mim, tente implementar a linguagem FP e verá que estou certo.
EDIT: Eu esqueci. Preguiça é INFERNO sem GC. Não acredita em mim? Experimente sem GC em, por exemplo, C ++. Você vai ver ... coisas
fonte
Haskell é uma linguagem de programação não rígida, mas a maioria das implementações usa chamada por necessidade (preguiça) para implementar a não rigidez. No call-by-need você só avalia as coisas quando elas são alcançadas durante o tempo de execução usando a maquinaria de "thunks" (expressões que esperam para serem avaliadas e então se sobrescrevem, ficando visíveis para que seu valor seja reutilizado quando necessário).
Portanto, se você implementar sua linguagem preguiçosamente usando thunks, terá adiado todo o raciocínio sobre a vida útil dos objetos até o último momento, que é o tempo de execução. Já que agora você não sabe nada sobre vidas, a única coisa que você pode fazer razoavelmente é coletar o lixo ...
fonte