Por que os compiladores não inserem desalocações automaticamente?


63

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.


125
E é isso, crianças, que ensinamos a teoria da computabilidade. ;)
Raphael

7
Este não é um problema de computabilidade, pois os humanos também não podem decidir em todos os casos. É um problema de completude; as instruções de desalocação contêm informações que, se removidas, não podem ser totalmente recuperadas pela análise, a menos que essa análise inclua informações sobre o ambiente de implementação e a operação esperada, que o código-fonte C não contém.
Nat

41
Não, é um problema de computabilidade. É indecidível se um determinado pedaço de memória deve ser desalocado. Para um programa fixo, nenhuma entrada do usuário ou outra interferência externa.
Andrej Bauer

11
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo . Todos os comentários que não abordam especificamente a questão e como ela pode ser aprimorada serão excluídos à vista.
Raphael

2
@BorisTreukhov, por favor, leve-o para a sala de bate-papo. Não, acho que Andrej não está dizendo que a análise de escape é "impossível" (embora seja difícil determinar exatamente o que isso significa nesse contexto). A análise de escape perfeitamente precisa é indecidível. Para todos: por favor, leve-o à sala de bate-papo . Poste aqui apenas comentários que visam melhorar a pergunta - outras discussões e comentários devem ser publicados na sala de bate-papo.
DW

Respostas:


81

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 para free()corrigir esses bugs, saber quando chamar free()esse compilador seria ainda mais difícil do que saber quando chamar free()ao usar um compilador que não tentou ajudar.


12
Temos uma pergunta que cobre a capacidade dos seres humanos de resolver problemas indecidíveis . Não posso dar um exemplo de um programa que seria compilado incorretamente porque isso depende do algoritmo que o compilador usa. Mas qualquer algoritmo produzirá uma saída incorreta para muitos programas diferentes.
precisa saber é o seguinte

11
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Gilles 'SO- stop be evil'

2
Gente, leve para conversar . Tudo o que não se relacionar diretamente com a resposta em si e como ela pode ser melhorada será excluído.
Raphael

2
Muitas coisas que os compiladores fazem feliz são indecidíveis em geral; não chegaríamos a lugar algum no mundo dos compiladores se sempre nos cedermos ao teorema de Rice.
Tikhon Jelvis

3
Isso é irrelevante. Se é indecidível para todos os compiladores, é indecidível para todos os humanos também. No entanto, esperamos que os humanos sejam inseridos free()corretamente.
Paul Draper

53

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:

  1. Compreendendo o programa e o problema. Os seres humanos podem colocar objetos com vida útil semelhante no mesmo objeto de alocação, por exemplo. Compiladores e coletores de lixo devem inferir isso, mas os seres humanos têm informações mais precisas.
  2. Usando seletivamente a contabilidade não-local (por exemplo, contagem de referência) ou outras técnicas especiais de alocação (por exemplo, zonas), somente quando necessário. Novamente, um ser humano pode saber disso onde um compilador deve inferir.
  3. Seriamente. Todo mundo sabe dos programas implantados no mundo real que apresentam vazamentos lentos, afinal. Ou, se não, às vezes os programas e as APIs internas precisam ser reestruturados ao longo da vida útil da memória, diminuindo a reutilização e a modularidade.

Os comentários não são para discussão prolongada. Se você deseja discutir declarativo versus funcional, faça-o no chat .
Gilles 'SO- stop be evil'

2
Essa é de longe a melhor resposta para a pergunta (que muitas respostas nem sequer abordam). Você poderia ter adicionado uma referência ao trabalho pioneiro de Hans Boehm no GC conservador : en.wikipedia.org/wiki/Boehm_garbage_collector . Outro ponto interessante é que a vivacidade dos dados (ou utilidade em um sentido estendido) pode ser definida com relação a uma semântica abstrata ou a um modelo de execução. Mas o tópico é realmente amplo.
babou

29

É 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.


3
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo . Todos os comentários adicionais que não abordam especificamente as deficiências desta resposta e como eles podem ser corrigidos serão excluídos à vista.
Raphael

23

Atualmente, nenhuma das respostas postadas está totalmente correta.

Por que os compiladores não inserem desalocações automaticamente?

  1. Alguns fazem. (Eu vou explicar mais tarde.)

  2. Trivialmente, você pode ligar free()imediatamente antes da saída do programa. Mas há uma necessidade implícita em sua pergunta para ligar o free()mais rápido possível.

  3. 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 .

  4. Um problema indecidível nem sempre pode ser resolvido em tempo finito por qualquer algoritmo, seja um compilador ou um humano.

  5. 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).

  6. 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.


11
Eu entendo como Rust faz isso. Mas nunca ouvi falar de um quarto que fizesse isso. Você pode elaborar?
Milton Silva

2
@MiltonSilva, Forth - pelo menos sua implementação original mais básica - tem apenas uma pilha, não uma pilha. Faz com que a alocação / desalocação mova o ponteiro da pilha de chamadas, uma tarefa que o compilador pode executar com facilidade. A quarta foi criada para atingir hardware muito simples, e às vezes a memória não dinâmica é tudo o que é viável. Obviamente, não é uma solução viável para programas não triviais.
Paul Draper

10

"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.


11
A falácia de aceitar a premissa da falibilidade humana e, ainda assim, supor que as máquinas pensantes criadas pelo homem ainda possam ser infalíveis (isto é, melhores que os humanos) é menos conhecida, mas mais intrigante. A única suposição da qual a ação pode prosseguir é que a mente humana tem o potencial de calcular perfeitamente.
Curinga

1. Eu nunca disse que máquinas pensantes podem ser infalíveis. Melhor do que os humanos é o que eles já são, em muitos casos. 2. A expectativa de perfeição (mesmo potencial) como pré-requisito para a ação é um absurdo.
André Souza Lemos

"Podemos algum dia, talvez não muito longe, ser substituídos por inteligência artificial no trabalho". Isso, em particular, não faz sentido. Os seres humanos são a fonte de intenções no sistema. Sem humanos, não há propósito para o sistema. "Inteligência Artificial" pode ser definida como a aparição de decisão inteligente no momento pelas máquinas, provocada de fato por decisões inteligentes de um programador ou designer de sistemas no passado. Se não houver manutenção (o que deve ser feito por uma pessoa), o AI (ou qualquer sistema deixado sem inspeção e totalmente automático) falhará.
Curinga

A intenção, tanto em humanos quanto em máquinas, sempre vem de fora .
André Souza Lemos

Totalmente falso. (E também, "fora" não define uma fonte. ) Ou você está declarando que a intenção, como tal, não existe realmente, ou está afirmando que a intenção existe, mas não vem de lugar algum. Talvez você acredite que a intenção possa existir independentemente do propósito? Nesse caso, você não entende a palavra "intenção". De qualquer maneira, uma demonstração em pessoa logo mudaria de idéia sobre esse assunto. Sairei depois desse comentário, pois as palavras por si só não podem trazer uma compreensão de "intenção", de modo que uma discussão mais aprofundada aqui é inútil.
Curinga

9

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.


Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
DW

2
Como isso é uma resposta para a (CS parte da) pergunta?
Raphael

6
@Raphael Ciência da computação não significa que devemos procurar respostas técnicas obscuras. Compiladores fazem muitas coisas que são impossíveis no caso geral. Se queremos um gerenciamento automático de memória, podemos implementá-lo de várias maneiras. C não faz isso, porque não deveria.
Jouni Sirén

9

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ície T*para armazenar memória e unique_ptr<T>garante que a memória seja liberada quando o objeto atingir o final do escopo, diferente da planície T*. O programador pode unique_ptr<T>passar a memória de um para outro, mas só pode haver um unique_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.


Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Raphael

6

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 #.


Em teoria, você poderia executar um GC antes de cada alocação.
precisa saber é o seguinte

4
@adrianN Mas, na prática, isso não é feito porque seria mental. O argumento de Graham ainda permanece: os GCs sempre incorrem em uma sobrecarga substancial, em termos de tempo de execução ou em termos de memória excedente necessária. Você pode ajustar esse equilíbrio para um dos extremos, mas fundamentalmente não pode remover a sobrecarga.
Konrad Rudolph

O "atraso" no momento em que a memória é liberada é mais um problema em um sistema de memória virtual do que em um sistema com recursos limitados. No primeiro caso, pode ser melhor para um programa usar 100 MB do que 200 MB, mesmo que o sistema tenha 200 MB disponíveis , mas no segundo caso não haverá benefício em executar o GC mais cedo do que o necessário, a menos que atrasos sejam mais aceitáveis ​​durante alguns partes do código do que durante outras.
Supercat

Não vejo como isso tenta responder à (parte CS) da pergunta.
Raphael

11
@ Rafael Expliquei um problema bem reconhecido com o princípio da coleta de lixo, que é (ou deveria ser) ensinado no CS como uma de suas desvantagens básicas. Até dei minha experiência pessoal de ter visto isso na prática, para mostrar que não é um problema puramente teórico. Se você não entendeu algo sobre isso, fico feliz em conversar com você para melhorar seu conhecimento sobre o assunto.
Graham

4

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.


4
"'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" - isso está claramente errado. a) Para muitos FOO, é trivial descobrir isso, por exemplo, variáveis ​​locais com semântica de valores. b) Você sugere que o programador sempre saiba disso, o que é claramente uma suposição excessivamente otimista; se fosse verdade, não teríamos erros graves devido ao manuseio incorreto da memória, é um software crítico para a segurança. Que, infelizmente, nós fazemos.
Raphael

Estou apenas sugerindo que a linguagem foi projetada para casos em que o programador não sabem FOO não é mais útil. Concordo, claramente, que isso geralmente não é verdade, e é por isso que precisamos ter análise estática e / ou coleta de lixo. Que, viva, nós fazemos. Mas a pergunta do OP é: quando essas coisas não são tão valiosas quanto os acordos comerciais codificados manualmente?
Travis Wilson

4

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.


Não vejo uma contribuição aqui.
babou

3

Em idiomas como C, o programador deve inserir chamadas gratuitamente. Por que o compilador não faz isso automaticamente?

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.


-1

Em idiomas como C, o programador deve inserir chamadas gratuitamente. Por que o compilador não faz isso automaticamente?

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.