Em idiomas como C, o programador deve inserir chamadas gratuitamente. Por que o compilador não faz isso automaticamente? Os seres humanos fazem isso em um período de tempo razoável (ignorando bugs), por isso não é impossível.
EDIT: Para referência futura, aqui está outra discussão que tem um exemplo interessante.
compilers
memory-management
garbage-collection
Milton Silva
fonte
fonte
Respostas:
Porque é indecidível se o programa usará a memória novamente. Isso significa que nenhum algoritmo pode determinar corretamente quando chamar
free()
em todos os casos, o que significa que qualquer compilador que tentasse fazer isso necessariamente produziria alguns programas com vazamentos de memória e / ou alguns programas que continuaram usando a memória liberada. Mesmo se você garantisse que o seu compilador nunca fizesse o segundo e permitisse ao programador inserir chamadas parafree()
corrigir esses bugs, saber quando chamarfree()
esse compilador seria ainda mais difícil do que saber quando chamarfree()
ao usar um compilador que não tentou ajudar.fonte
free()
corretamente.Como David Richerby observou com razão, o problema é indecidível em geral. A vivacidade do objeto é uma propriedade global do programa e, em geral, pode depender das entradas do programa.
Até a coleta dinâmica de lixo precisa é um problema indecidível! Todos os coletores de lixo do mundo real usam a acessibilidade como uma aproximação conservadora para determinar se um objeto alocado será ou não necessário no futuro. É uma boa aproximação, mas é uma aproximação, no entanto.
Mas isso é verdade apenas em geral. Um dos mais notórios casos de copiadores no ramo de ciência da computação é "geralmente é impossível, portanto, não podemos fazer nada". Pelo contrário, existem muitos casos em que é possível avançar.
As implementações baseadas na contagem de referências estão muito próximas dos "compiladores que inserem desalocações", de modo que é difícil dizer a diferença. A contagem automática de referência do LLVM (usada em Objective-C e Swift ) é um exemplo famoso.
A inferência de região e a coleta de lixo em tempo de compilação são áreas de pesquisa ativas atuais. É muito mais fácil em linguagens declarativas como ML e Mercury , onde você não pode modificar um objeto depois que ele é criado.
Agora, no tópico humanos, existem três maneiras principais de gerenciar manualmente a vida útil da alocação:
fonte
É um problema de incompletude, não um problema de indecidibilidade
Embora seja verdade que o posicionamento ideal das declarações de desalocação seja indecidível, esse não é o problema aqui. Como é indecidível para humanos e compiladores, é impossível sempre selecionar conscientemente o posicionamento ideal da desalocação, independentemente de ser um processo manual ou automático. E como ninguém é perfeito, um compilador suficientemente avançado deve ser capaz de superar os humanos ao adivinhar posicionamentos aproximadamente ideais. Portanto, a indecidibilidade não é o motivo pelo qual precisamos de declarações explícitas de desalocação .
Há casos em que o conhecimento externo informa a colocação da declaração de desalocação. A remoção dessas instruções é equivalente a remover parte da lógica operacional e pedir a um compilador para gerar automaticamente essa lógica é equivalente a pedir que ele adivinhe o que você está pensando.
Por exemplo, digamos que você esteja escrevendo um REPL (Read-Evaluate-Print-Loop) : o usuário digita um comando e o programa o executa. O usuário pode alocar / desalocar memória digitando comandos no seu REPL. Seu código-fonte especificaria o que o REPL deve fazer para cada possível comando do usuário, incluindo desalocação quando o usuário digitar o comando para ele.
Mas se o código-fonte C não fornecer um comando explícito para desalocação, o compilador precisará inferir que deve executar a desalocação quando o usuário inserir o comando apropriado no REPL. Esse comando é "desalocar", "grátis" ou algo mais? O compilador não tem como saber o que você deseja que o comando seja. Mesmo se você programar em lógica para procurar essa palavra de comando e o REPL a encontrar, o compilador não tem como saber que deve responder a ele com desalocação, a menos que você o informe explicitamente no código-fonte.
tl; dr O problema é que o código fonte C não fornece conhecimento externo ao compilador. Indecidibilidade não é o problema, porque existe se o processo é manual ou automatizado.
fonte
Atualmente, nenhuma das respostas postadas está totalmente correta.
Alguns fazem. (Eu vou explicar mais tarde.)
Trivialmente, você pode ligar
free()
imediatamente antes da saída do programa. Mas há uma necessidade implícita em sua pergunta para ligar ofree()
mais rápido possível.O problema de quando chamar
free()
qualquer programa C assim que a memória estiver inacessível é indecidível, ou seja, para qualquer algoritmo que forneça a resposta em tempo finito, há um caso que não cobre. Isso - e muitas outras indecisões de programas arbitrários - podem ser comprovadas a partir do Problema da Parada .Um problema indecidível nem sempre pode ser resolvido em tempo finito por qualquer algoritmo, seja um compilador ou um humano.
Os seres humanos (tentam) escrevem em um subconjunto de programas C que podem ser verificados quanto à correção da memória pelo algoritmo (eles mesmos).
Alguns idiomas alcançam o número 1 ao criar o número 5 no compilador. Eles não permitem programas com usos arbitrários de alocação de memória, mas um subconjunto decidível deles. Foth e Rust são dois exemplos de linguagens que têm alocação de memória mais restritiva que Cs
malloc()
, que podem (1) detectar se um programa está gravado fora do seu conjunto decidível (2) inserir desalocações automaticamente.fonte
"Os humanos fazem isso, então não é impossível" é uma falácia bem conhecida. Não entendemos necessariamente (muito menos controlamos) as coisas que criamos - o dinheiro é um exemplo comum. Tendemos a superestimar (às vezes de maneira dramática) nossas chances de sucesso em questões tecnológicas, especialmente quando fatores humanos parecem estar ausentes.
O desempenho humano na programação de computadores é muito ruim , e o estudo da ciência da computação (ausente em muitos programas de educação profissional) ajuda a entender por que esse problema não tem uma solução simples. Podemos algum dia, talvez não muito longe, ser substituídos por inteligência artificial no trabalho. Mesmo assim, não haverá um algoritmo geral que acerte a desalocação automaticamente, o tempo todo.
fonte
A falta de gerenciamento automático de memória é um recurso do idioma.
C não deveria ser uma ferramenta para escrever software facilmente. É uma ferramenta para fazer o computador fazer o que você pedir. Isso inclui alocar e desalocar memória no momento de sua escolha. C é uma linguagem de baixo nível que você usa quando deseja controlar o computador com precisão ou quando deseja fazer as coisas de uma maneira diferente da esperada pelos projetistas de linguagem / biblioteca padrão.
fonte
A questão é principalmente um artefato histórico, não uma impossibilidade de implementação.
A maneira como a maioria dos compiladores C constrói código é para que o compilador veja apenas cada arquivo de origem por vez; nunca vê o programa inteiro de uma só vez. Quando um arquivo de origem chama uma função de outro arquivo de origem ou de uma biblioteca, tudo o que o compilador vê é o arquivo de cabeçalho com o tipo de retorno da função, não o código real da função. Isso significa que quando existe uma função que retorna um ponteiro, o compilador não tem como saber se a memória que o ponteiro está apontando precisa ser liberada ou não. As informações para decidir que não são mostradas para o compilador nesse momento. Um programador humano, por outro lado, é livre para procurar o código fonte da função ou a documentação para descobrir o que precisa ser feito com o ponteiro.
Se você procurar por linguagens de baixo nível mais modernas, como C ++ 11 ou Rust, descobrirá que elas resolveram o problema principalmente ao tornar explícita a propriedade da memória no tipo de ponteiro. No C ++, você usaria um em
unique_ptr<T>
vez de uma planícieT*
para armazenar memória eunique_ptr<T>
garante que a memória seja liberada quando o objeto atingir o final do escopo, diferente da planícieT*
. O programador podeunique_ptr<T>
passar a memória de um para outro, mas só pode haver umunique_ptr<T>
apontando para a memória. Portanto, é sempre claro quem é o dono da memória e quando ela precisa ser liberada.O C ++, por motivos de compatibilidade com versões anteriores, ainda permite o gerenciamento manual de memória com estilo antigo e, portanto, a criação de bugs ou maneiras de burlar a proteção de um
unique_ptr<T>
. A ferrugem é ainda mais rigorosa na medida em que impõe regras de propriedade da memória por meio de erros do compilador.Quanto à indecidibilidade, o problema de interrupção e similares, sim, se você seguir a semântica C, não será possível decidir para todos os programas quando a memória deve ser liberada. No entanto, para a maioria dos programas atuais, não exercícios acadêmicos ou software de buggy, seria absolutamente possível decidir quando liberar e quando não. Afinal, essa é a única razão pela qual os humanos podem descobrir quando libertar ou não em primeiro lugar.
fonte
Outras respostas focaram em se é possível fazer a coleta de lixo, alguns detalhes de como é feito e alguns dos problemas.
Uma questão que ainda não foi abordada é o inevitável atraso na coleta de lixo. Em C, quando um programador chama free (), essa memória fica imediatamente disponível para reutilização. (Pelo menos em teoria!) Para que um programador possa liberar sua estrutura de 100 MB, alocar outra estrutura de 100 MB um milissegundo mais tarde e esperar que o uso geral da memória permaneça o mesmo.
Isso não é verdade com a coleta de lixo. Os sistemas de coleta de lixo têm algum atraso no retorno da memória não utilizada ao heap, e isso pode ser significativo. Se sua estrutura de 100 MB ficar fora do escopo e, um milissegundo depois, seu programa configurar outra estrutura de 100 MB, você poderá esperar razoavelmente que seu sistema esteja usando 200 MB por um curto período. Esse "período curto" pode levar milissegundos ou segundos, dependendo do sistema, mas ainda há um atraso.
Se você estiver executando em um PC com GB de RAM e memória virtual, é claro que provavelmente nunca perceberá isso. Se você estiver executando em um sistema com recursos mais limitados (digamos, um sistema incorporado ou um telefone), isso é algo que você precisa levar a sério. Isso não é apenas teórico - eu pessoalmente vi isso criar problemas (como travar o tipo de problemas do dispositivo) ao trabalhar em um sistema WinCE usando o .NET Compact Framework e desenvolvendo em C #.
fonte
A questão presume que uma desalocação é algo que o programador deve deduzir de outras partes do código-fonte. Não é. "Neste ponto do programa, a referência de memória FOO não é mais útil" são informações conhecidas apenas na mente do programador até que ele seja codificado (em linguagens procedurais) em uma declaração de desalocação.
Não é teoricamente diferente de qualquer outra linha de código. Por que os compiladores não inserem automaticamente "Neste ponto do programa, verifique a entrada do registrador BAR" ou "se a chamada de função retornar diferente de zero, sair da sub-rotina atual" ? Do ponto de vista do compilador, a razão é "incompletude", como mostrado nesta resposta . Mas qualquer programa sofre de incompletude quando o programador não conta tudo o que sabe.
Na vida real, as desalocações são trabalho pesado ou clichê; nosso cérebro os preenche automaticamente e resmunga sobre isso, e o sentimento "o compilador poderia fazê-lo tão bem ou melhor" é verdadeiro. Em teoria, no entanto, esse não é o caso, embora felizmente outras línguas nos dêem mais opções de teoria.
fonte
O que é feito: Há coleta de lixo e compiladores usando a contagem de referência (Objective-C, Swift). Aqueles que fazem a contagem de referência precisam da ajuda do programador, evitando fortes ciclos de referência.
A resposta real para o "porquê" é que os escritores do compilador não descobriram uma maneira suficientemente boa e rápida o suficiente para torná-lo utilizável em um compilador. Como os escritores de compiladores geralmente são bastante inteligentes, você pode concluir que é muito, muito difícil encontrar uma maneira que seja boa o suficiente e rápida o suficiente.
Uma das razões pelas quais é muito, muito difícil é, obviamente, que é indecidível. Na ciência da computação, quando falamos em "decidibilidade", queremos dizer "tomar a decisão certa". É claro que programadores humanos podem facilmente decidir onde desalocar memória, porque eles não estão limitados a decisões corretas . E eles costumam tomar decisões erradas.
fonte
Como a vida útil de um bloco de memória é uma decisão do programador, não do compilador.
É isso aí. Esse é o design do C. O compilador não pode saber qual era a intenção de alocar um bloco de memória. Os humanos podem fazê-lo, porque sabem o propósito de cada bloco de memória e quando esse objetivo é atendido, para que possa ser liberado. Isso faz parte do design do programa que está sendo escrito.
C é uma linguagem de baixo nível, portanto, instâncias de passar um bloco de memória para outro processo ou mesmo para outro processador são bastante frequentes. Em casos extremos, um programador pode alocar intencionalmente um pedaço de memória e nunca mais usá-lo apenas para pressionar a memória de outras partes do sistema. O compilador não tem como saber se o bloco ainda é necessário.
fonte
Em C e em muitos outros idiomas, existe de fato um recurso para fazer o compilador fazer o equivalente a isso nos casos em que fica claro no tempo de compilação quando deve ser feito: uso de variáveis de duração automática (variáveis locais comuns) . O compilador é responsável por organizar espaço suficiente para essas variáveis e liberar esse espaço quando a vida útil (bem definida) terminar.
Com matrizes de comprimento variável sendo um recurso C desde C99, os objetos de duração automática servem, em princípio, substancialmente todas as funções em C que objetos alocados dinamicamente de duração computável. Na prática, é claro, as implementações de C podem colocar limites práticos significativos no uso de VLAs - ou seja, seu tamanho pode ser limitado como resultado de serem alocados na pilha - mas essa é uma consideração de implementação, não uma consideração de design de linguagem.
Os objetos cujo uso pretendido impede a duração automática são precisamente aqueles cujo tempo de vida não pode ser determinado em tempo de compilação.
fonte