Eu realmente gosto do gerenciamento de memória com base em escopo (SBMM), ou RAII , pois é mais comum (confusamente?) Referido pela comunidade C ++. Até onde eu sei, exceto C ++ (e C), não há outra linguagem convencional em uso hoje que faça do SBMM / RAII seu principal mecanismo de gerenciamento de memória e, em vez disso, eles preferem usar a coleta de lixo (GC).
Acho isso bastante confuso, pois
- O SBMM torna os programas mais determinísticos (você pode dizer exatamente quando um objeto é destruído);
- nas linguagens que usam o GC, muitas vezes é necessário o gerenciamento manual de recursos (consulte o fechamento de arquivos em Java, por exemplo), que derruba parcialmente o objetivo do GC e também é passível de erros;
- a memória heap também pode (muito elegantemente, imo) ser vinculada ao escopo (veja
std::shared_ptr
em C ++).
Por que o SBMM não é mais amplamente usado? Quais são as suas desvantagens?
finalize()
método de um objeto seja chamado antes da coleta de lixo. Com efeito, isso cria a mesma classe de problema que a coleta de lixo deve resolver.Respostas:
Vamos começar postulando que a memória é de longe (dezenas, centenas ou até milhares de vezes) mais comum do que todos os outros recursos combinados. Cada variável, objeto, membro do objeto precisa de alguma memória alocada e liberada posteriormente. Para cada arquivo aberto, você cria dezenas a milhões de objetos para armazenar os dados extraídos do arquivo. Todo fluxo TCP acompanha um número ilimitado de cadeias de bytes temporárias criadas para serem gravadas no fluxo. Estamos na mesma página aqui? Ótimo.
Para que o RAII funcione (mesmo que você tenha ponteiros inteligentes prontos para cada caso de uso sob o sol), é necessário ter a propriedade correta. Você precisa analisar quem deve ser o proprietário desse ou daquele objeto, quem não deve e quando a propriedade deve ser transferida de A para B. Claro, você pode usar a propriedade compartilhada para tudo , mas depois emula um GC por meio de ponteiros inteligentes. Nesse ponto, fica muito mais fácil e rápido criar o GC no idioma.
A coleta de lixo libera você dessa preocupação com a memória de longe o recurso mais comumente usado. Claro, você ainda precisa tomar a mesma decisão para outros recursos, mas esses são muito menos comuns (veja acima), e a propriedade complicada (por exemplo, compartilhada) também é menos comum. A carga mental é reduzida significativamente.
Agora, você cita algumas desvantagens de tornar todos os valores coletados como lixo. No entanto, a integração de GC e tipos de valor com segurança de memória com RAII em um idioma é extremamente difícil, portanto, talvez seja melhor migrar essas compensações por outros meios?
A perda de determinismo acaba por não ser tão ruim na prática, porque afeta apenas a vida determinística do objeto . Conforme descrito no próximo parágrafo, a maioria dos recursos (além da memória, que é abundante e pode ser reciclada preguiçosamente) não está vinculada ao tempo de vida do objeto nessas linguagens. Existem alguns outros casos de uso, mas eles são raros na minha experiência.
Atualmente, seu segundo ponto, gerenciamento manual de recursos, é abordado por meio de uma declaração que executa a limpeza com base no escopo, mas não acopla essa limpeza ao tempo de vida do objeto (portanto, não interage com o GC e a segurança da memória). Isso está
using
em C #,with
em Python,try
com recursos nas versões recentes do Java.fonte
using
declarações são possíveis apenas localmente. É impossível limpar os recursos mantidos nas variáveis membros dessa maneira.using
é uma piada comparada à RAII, só para você saber.O RAII também segue do gerenciamento automático de memória de contagem de referência, por exemplo, conforme usado pelo Perl. Embora a contagem de referência seja fácil de implementar, determinística e de alto desempenho, ela não pode lidar com referências circulares (elas causam um vazamento), razão pela qual não é comumente usada.
Os idiomas coletados pelo lixo não podem usar RAII diretamente, mas geralmente oferecem sintaxe com um efeito equivalente. Em Java, temos a instrução try-with-ressource
que chama automaticamente
.close()
o recurso na saída do bloco. O C # possui aIDisposable
interface, que permite.Dispose()
ser chamada ao sair de umausing (...) { ... }
instrução. Python tem awith
declaração:que funciona de maneira semelhante. Em uma análise interessante, o método de abertura de arquivo do Ruby recebe um retorno de chamada. Depois que o retorno de chamada foi executado, o arquivo é fechado.
Eu acho que o Node.js usa a mesma estratégia.
fonte
with-open-filehandle
funções que abrem o arquivo, produzem uma função e, ao retornar a função, fecham o arquivo novamente.Na minha opinião, a vantagem mais convincente da coleta de lixo é que ela permite a composição . A correção do gerenciamento de memória é uma propriedade local no ambiente de coleta de lixo. Você pode examinar cada parte isoladamente e determinar se ela pode vazar memória. Combine qualquer número de peças com correção de memória e elas permanecerão corretas.
Quando você conta com a contagem de referências, perde essa propriedade. Se seu aplicativo pode vazar memória torna-se uma propriedade global de todo o aplicativo com contagem de referência. Toda nova interação entre partes tem a possibilidade de usar a propriedade errada e interromper o gerenciamento de memória.
Tem um efeito muito visível no design de programas nas diferentes linguagens. Os programas nas linguagens GC tendem a ser um pouco mais de sopas de objetos com muitas interações, enquanto nas linguagens sem GC costumamos preferir partes estruturadas com interações estritamente controladas e limitadas entre elas.
fonte
Os fechamentos são uma característica essencial de praticamente todas as línguas modernas. Eles são muito fáceis de implementar com o GC e muito difíceis (embora não impossíveis) de acertar com o RAII, pois uma das principais características é que eles permitem abstrair durante a vida útil de suas variáveis!
O C ++ só os obteve 40 anos depois que todo mundo fez, e foi preciso muito trabalho duro de muitas pessoas inteligentes para acertá-los. Por outro lado, muitas linguagens de script projetadas e implementadas por pessoas com zero conhecimento em design e implementação de linguagens de programação as possuem.
fonte
[&]
sintaxe. Qualquer programador C ++ já associa o&
sinal a referências e conhece referências obsoletas.Para a maioria dos programadores, o SO não é determinístico, seu alocador de memória é não determinístico e a maioria dos programas que eles escrevem são simultâneos e, portanto, inerentemente não determinísticos. Adicionar a restrição de que um destruidor é chamado exatamente no final do escopo, e não um pouco antes ou um pouco depois, não é um benefício prático significativo para a grande maioria dos programadores.
Veja
using
em C # euse
em F #.Em outras palavras, você pode pegar o heap, que é uma solução de uso geral, e alterá-lo para funcionar apenas em um caso específico que seja seriamente limitador. Isso é verdade, é claro, mas é inútil.
O SBMM limita o que você pode fazer:
O SBMM cria o problema de funarg para cima com fechamentos lexicais de primeira classe, e é por isso que os fechamentos são populares e fáceis de usar em linguagens como C #, mas raros e complicados em C ++. Observe que há uma tendência geral para o uso de construções funcionais na programação.
O SBMM requer destruidores e eles impedem chamadas finais adicionando mais trabalho a ser feito antes que uma função possa retornar. As chamadas de cauda são úteis para máquinas de estado extensíveis e são fornecidas por coisas como o .NET.
Algumas estruturas de dados e algoritmos são notoriamente difíceis de implementar usando o SBMM. Basicamente em qualquer lugar que os ciclos ocorram naturalmente. Mais notavelmente algoritmos gráficos. Você efetivamente acaba escrevendo seu próprio GC.
A programação simultânea é mais difícil porque o fluxo de controle e, portanto, a vida útil do objeto são inerentemente não determinísticos aqui. As soluções práticas nos sistemas de passagem de mensagens tendem a ser cópias profundas das mensagens e o uso de vidas excessivamente longas.
O SBMM mantém os objetos ativos até o final de seu escopo no código-fonte, que geralmente é maior que o necessário e pode ser muito maior que o necessário. Isso aumenta a quantidade de lixo flutuante (objetos inacessíveis aguardando para serem reciclados). Por outro lado, o rastreamento da coleta de lixo tende a liberar objetos logo após a última referência a eles desaparecer, o que pode ser muito mais rápido. Consulte Mitos sobre gerenciamento de memória: rapidez .
O SBMM é tão limitador que os programadores precisam de uma rota de fuga para situações em que não é possível aninhar a vida útil. No C ++,
shared_ptr
oferece uma rota de fuga, mas pode ser ~ 10x mais lenta que rastrear a coleta de lixo . Portanto, o uso do SBMM em vez do GC colocaria a maioria das pessoas erradas na maioria das vezes. Isso não quer dizer, no entanto, que seja inútil. O SBMM ainda é valioso no contexto de sistemas e programação incorporada, onde os recursos são limitados.No FWIW, você pode conferir Forth e Ada e ler o trabalho de Nicolas Wirth.
fonte
shared_ptr
só é raro em C ++ porque é muito lento. Em segundo lugar, é uma comparação de maçãs e laranjas (como o artigo que citei já mostrou) porqueshared_ptr
é muitas vezes mais lenta que um GC de produção. Em terceiro lugar, os GCs não são onipresentes e são evitados em softwares como o LMax e o mecanismo FIX da Rapid Addition.Olhando para algum índice de popularidade como o TIOBE (que é discutível, é claro, mas acho que seu tipo de pergunta pode ser usado), primeiro você vê que ~ 50% dos 20 principais são "linguagens de script" ou "dialetos SQL" ", onde a" facilidade de uso "e os meios de abstração têm muito mais importância do que o comportamento determinístico. Dos idiomas "compilados" restantes, existem cerca de 50% dos idiomas com SBMM e ~ 50% sem. Portanto, ao tirar as linguagens de script do seu cálculo, eu diria que sua suposição está errada, entre as linguagens compiladas as que possuem o SBMM são tão populares quanto as que não possuem.
fonte
Uma grande vantagem de um sistema de GC que ninguém mencionou ainda é que é garantida uma referência em um sistema de GC para manter sua identidade enquanto existir . Se alguém chamar
IDisposable.Dispose
(.NET) ouAutoCloseable.Close
(Java) um objeto enquanto existirem cópias da referência, essas cópias continuarão se referindo ao mesmo objeto. O objeto não será mais útil para nada, mas as tentativas de usá-lo terão um comportamento previsível controlado pelo próprio objeto. Por outro lado, em C ++, se o código chamadelete
um objeto e depois tenta usá-lo, todo o estado do sistema fica totalmente indefinido.Outra coisa importante a ser observada é que o gerenciamento de memória baseado em escopo funciona muito bem para objetos com propriedade claramente definida. Funciona muito menos bem e, às vezes, mal, com objetos que não possuem propriedade definida. Em geral, objetos mutáveis devem ter proprietários, enquanto objetos imutáveis não precisam, mas há um problema: é muito comum o código usar uma instância de tipos mutáveis para armazenar dados imutáveis, garantindo que nenhuma referência seja exposta a eles. código que pode alterar a instância. Nesse cenário, instâncias da classe mutável podem ser compartilhadas entre vários objetos imutáveis e, portanto, não possuem propriedade clara.
fonte
Primeiro, é muito importante perceber que igualar RAII a SBMM. ou mesmo para SBRM. Uma das qualidades mais essenciais (e menos conhecidas ou menos valorizadas) da RAII é o fato de tornar 'ser um recurso' uma propriedade que NÃO é transitiva para a composição.
A postagem do blog a seguir discute esse aspecto importante do RAII e o contrasta com o surgimento de recursos em idiomas GCed que usam GC não determinístico.
http://minorfs.wordpress.com/2011/04/29/why-garbage-collection-is-anti-productive/
É importante observar que, embora o RAII seja usado principalmente em C ++, o Python (finalmente a versão não baseada em VM) possui destruidores e GC determinístico que permitem que o RAII seja usado junto com o GC. Melhor dos dois mundos, se fosse.
fonte
File.ReadLines file |> Seq.length
onde as abstrações lidam com o fechamento para mim. Bloqueios e threads que substituí pelo .NETTask
e F #MailboxProcessor
. Todo esse "explodimos a quantidade de gerenciamento manual de recursos" é apenas um completo disparate.