Se eu precisar usar um pedaço de memória durante toda a vida útil do meu programa, é realmente necessário liberá-lo imediatamente antes do término do programa?

67

Em muitos livros e tutoriais, eu ouvi a prática do gerenciamento de memória enfatizar e senti que algumas coisas misteriosas e terríveis aconteceriam se eu não liberasse memória depois de terminar de usá-lo.

Não posso falar por outros sistemas (embora, para mim, seja razoável supor que eles adotem uma prática semelhante), mas pelo menos no Windows, o Kernel basicamente garante a limpeza da maioria dos recursos (com exceção de alguns poucos) usados ​​pelo um programa após o término do programa. O que inclui memória de pilha, entre várias outras coisas.

Entendo por que você deseja fechar um arquivo depois de usá-lo para disponibilizá-lo ao usuário ou por que você deseja desconectar um soquete conectado a um servidor para economizar largura de banda, mas parece bobagem você precisa gerenciar toda a sua memória usada pelo seu programa.

Agora, eu concordo que essa questão é ampla, pois como você deve lidar com sua memória se baseia em quanta memória você precisa e quando precisa, então restringirei o escopo desta pergunta a este: Se eu precisar usar um pedaço de memória durante toda a vida útil do meu programa, é realmente necessário liberá-lo imediatamente antes do término do programa?

Editar: A pergunta sugerida como duplicada era específica da família de sistemas operacionais Unix. Sua resposta principal chegou a especificar uma ferramenta específica para Linux (por exemplo, Valgrind). Esta pergunta pretende abranger a maioria dos sistemas operacionais não incorporados "normais" e por que é ou não uma boa prática liberar memória necessária durante toda a vida útil de um programa.

Capitão Óbvio
fonte
19
Você marcou este idioma como agnóstico, mas em muitos idiomas (por exemplo, java) não é necessário liberar memória manualmente; isso acontece automaticamente algum tempo após a última referência a um objeto sai do escopo
Richard Tingle
3
e, claro, você pode escrever C ++ sem qualquer único apagar e que seria bom para a memória
Nikko
5
@RichardTingle Embora eu não consiga pensar em nenhum outro idioma que não seja C e talvez C ++, a tag independente de idioma foi criada para cobrir todos os idiomas que não possuem muitos utilitários de coleta de lixo embutidos. Isso é reconhecidamente raro hoje em dia. Você pode argumentar que agora pode implementar um sistema desse tipo em C ++, mas ainda tem a opção de não excluir um pedaço de memória.
CaptainObvious
4
Você escreve: "basicamente é garantido que o Kernel limpa todos os recursos [...] após o término do programa". Isso geralmente é falso, porque nem tudo é memória, identificador ou objeto de kernel. Mas é verdade para a memória, à qual você restringe imediatamente sua pergunta.
Eric Towers /

Respostas:

108

Se eu precisar usar um pedaço de memória durante toda a vida útil do meu programa, é realmente necessário liberá-lo imediatamente antes do término do programa?

Não é obrigatório, mas pode ter benefícios (além de algumas desvantagens).

Se o programa alocar memória uma vez durante o tempo de execução e, de outra forma, nunca a liberar até o término do processo, pode ser uma abordagem sensata não liberar a memória manualmente e confiar no sistema operacional. Em todos os sistemas operacionais modernos que conheço, isso é seguro; no final do processo, toda a memória alocada é retornada com segurança ao sistema.
Em alguns casos, a não limpeza explícita da memória alocada pode ser notavelmente mais rápida do que a limpeza.

No entanto, liberando explicitamente toda a memória no final da execução,

  • durante a depuração / teste, as ferramentas de detecção de vazamento de mem não mostrarão "falsos positivos"
  • pode ser muito mais fácil mover o código que usa a memória juntamente com alocação e desalocação para um componente separado e usá-lo posteriormente em um contexto diferente, onde o tempo de uso da memória precisa ser controlado pelo usuário do componente

A vida útil dos programas pode mudar. Talvez seu programa seja um pequeno utilitário de linha de comando hoje, com uma vida útil típica de menos de 10 minutos, e aloque memória em partes de alguns kb a cada 10 segundos - portanto, não é necessário liberar nenhuma memória alocada antes que o programa termine. Posteriormente, o programa é alterado e obtém um uso prolongado como parte de um processo do servidor com uma vida útil de várias semanas - portanto, não liberar memória não utilizada no meio não é mais uma opção; caso contrário, o programa começará a consumir toda a memória disponível do servidor ao longo do tempo . Isso significa que você precisará revisar todo o programa e adicionar o código de desalocação posteriormente. Se você tiver sorte, essa é uma tarefa fácil; caso contrário, pode ser tão difícil que há grandes chances de você perder um lugar. E quando você estiver nessa situação, desejará ter adicionado o "free"

De maneira mais geral, escrever códigos de alocação e desalocação relacionados sempre conta em pares como um "bom hábito" entre muitos programadores: fazendo isso sempre, você diminui a probabilidade de esquecer o código de desalocação em situações em que a memória deve ser liberada.

Doc Brown
fonte
12
Bom ponto sobre o desenvolvimento de bons hábitos de programação.
Lawrence
4
Geralmente, concordo plenamente com este conselho. Manter bons hábitos com a memória é muito importante. No entanto, acho que há exceções. Por exemplo, se o que você alocou é algum gráfico maluco que levará alguns segundos para percorrer e liberar "corretamente", você estará melhorando a experiência do usuário ao finalizar o programa e deixar que o SO o destrua.
GrandOpener
11
É seguro em todos os sistemas operacionais "normais" modernos (não incorporados com proteção de memória) e seria um bug sério se não fosse. Se um processo não privilegiado puder perder permanentemente a memória que o sistema operacional não recuperará, executá-lo várias vezes poderá esgotar a memória do sistema. A integridade a longo prazo do sistema operacional não pode depender da falta de bugs em programas não privilegiados. (é claro, isso é ignorar coisas como Unix segmentos de memória compartilhada, que só são apoiados por espaço de troca na melhor das hipóteses.)
Peter Cordes
7
@GrandOpener Nesse caso, você pode preferir usar algum tipo de alocador baseado em região para essa árvore, para que você possa alocá-lo da maneira normal e apenas desalocar a região inteira de uma só vez quando chegar a hora, em vez de atravessá-la e liberar pouco a pouco. Isso ainda é "adequado".
Thomas
3
Além disso, se a memória realmente precisar existir durante toda a vida útil do programa, pode ser uma alternativa sensata criar uma estrutura na pilha, por exemplo, in main.
Kyle Strand
11

Liberar memória no final de uma execução de programas é apenas uma perda de tempo da CPU. É como arrumar uma casa antes de retirá-la da órbita.

No entanto, às vezes, o que era um programa de execução curta pode se tornar parte de um programa de execução muito mais longo. Então, liberar coisas se torna necessário. Se isso não foi pensado, pelo menos até certo ponto, pode envolver retrabalho substancial.

Uma solução inteligente para isso é o "talloc", que permite fazer uma grande quantidade de alocações de memória e depois jogar tudo fora com uma única chamada.

Peter Green
fonte
11
Supondo que você saiba que seu sistema operacional não tem seus próprios vazamentos.
WGroleau
5
"desperdício de tempo da CPU" Embora seja uma quantidade muito pequena de tempo. freegeralmente é muito mais rápido que malloc.
Paul Draper
@PaulDraper Sim, a perda de tempo trocando tudo de volta é muito mais significativa.
Deduplicator 9/10
5

Você pode usar uma linguagem com coleta de lixo (como Scheme, Ocaml, Haskell, Common Lisp e até Java, Scala, Clojure).

(Na maioria das linguagens GC-ed, não há nenhuma maneira para explicitamente e manualmente memória livre Às vezes, alguns valores pode ser! Finalizado , por exemplo, o GC eo sistema de execução fecharia um valor identificador de arquivo quando esse valor é inacessível, mas isso não é certo, não confiável, e você deve fechar explicitamente seus identificadores de arquivo, pois a finalização nunca é garantida)

Você também pode, para o seu programa codificado em C (ou mesmo C ++), usar o coletor de lixo conservador da Boehm . Você substituiria todo o seu mallocpor GC_malloc e não se incomodaria em freeusar qualquer ponteiro. Claro que você precisa entender os prós e contras do uso do GC da Boehm. Leia também o manual do GC .

Gerenciamento de memória é uma propriedade global de um programa. De alguma forma, ele (e a vivacidade de alguns dados) é não-composicional e não-modular, pois é uma propriedade inteira do programa.

Por fim, como outros apontaram, a utilização explícita da freezona de memória C alocada ao heap é uma boa prática. Para um programa de brinquedo que não aloca muita memória, você pode até decidir não ter freememória (já que, quando o processo terminar, seus recursos, incluindo o espaço de endereço virtual , serão liberados pelo sistema operacional).

Se eu precisar usar um pedaço de memória durante toda a vida útil do meu programa, é realmente necessário liberá-lo imediatamente antes do término do programa?

Não, você não precisa fazer isso. E muitos programas do mundo real não se preocupam em liberar alguma memória necessária durante toda a vida útil (em particular, o compilador GCC não está liberando parte de sua memória). Entretanto, quando você faz isso (por exemplo, você não se incomoda em freeusar uma parte específica de dados alocados dinamicamente em C ), é melhor comentar esse fato para facilitar o trabalho de futuros programadores no mesmo projeto. Costumo recomendar que a quantidade de memória não-liberada permaneça limitada e, geralmente, relativamente pequena para total da memória heap usada.

Observe que o sistema freegeralmente não libera memória para o sistema operacional (por exemplo, chamando munmap (2) em sistemas POSIX), mas geralmente marca uma zona de memória como reutilizável no futuro malloc. Em particular, o espaço de endereço virtual (por exemplo, como visto /proc/self/mapsno Linux, consulte proc (5) ....) pode não diminuir depois free(portanto, utilitários gostam psou topestão relatando a mesma quantidade de memória usada para o seu processo).

Basile Starynkevitch
fonte
3
"Às vezes, alguns valores podem ser finalizados, por exemplo, o GC e o sistema de tempo de execução fecham um valor de identificador de arquivo quando esse valor é inacessível" Você pode enfatizar que na maioria dos idiomas do GCed não há garantia de que alguma memória será recuperada, nem qualquer finalizador já executado, mesmo na saída: stackoverflow.com/questions/7880569/…
Deduplicator
Pessoalmente, prefiro essa resposta, pois ela nos incentiva a usar o GC (ou seja, uma abordagem mais automatizada).
Ta Thanh Dinh
3
@tathanhdinh Mais automatizado, embora exclusivamente para memória. Completamente manual, para todos os outros recursos. O GC trabalha negociando determinismo e memória para um manuseio conveniente da memória. E não, os finalizadores não ajudam muito e têm seus próprios problemas.
Deduplicator
3

Não é necessário , pois você não falhará em executar seu programa corretamente se não conseguir. No entanto, há razões pelas quais você pode optar, se tiver uma chance.

Um dos casos mais poderosos com os quais me deparo (repetidamente) é que alguém escreve um pequeno pedaço de código de simulação que é executado em seu executável. Eles dizem "gostaríamos que esse código fosse integrado à simulação". Depois, pergunto a eles como planejam reinicializar entre as corridas de monte carlo, e eles me olham sem entender. "O que você quer dizer com reinicializar? Você acabou de executar o programa com novas configurações?"

Às vezes, uma limpeza limpa facilita muito o uso do seu software. No caso de muitos exemplos, você presume que nunca precisa limpar algo e faz suposições sobre como pode lidar com os dados e sua vida útil em torno dessas suposições. Quando você muda para um novo ambiente em que essas presunções não são válidas, algoritmos inteiros podem não funcionar mais.

Para um exemplo de como as coisas podem ficar estranhas, veja como as linguagens gerenciadas lidam com a finalização no final dos processos ou como o C # lida com a interrupção programática dos domínios de aplicativos. Eles se prendem a nós porque há suposições que caem nas frestas nesses casos extremos.

Cort Ammon
fonte
1

Ignorando idiomas nos quais você não libera memória manualmente de qualquer maneira ...

O que você considera um "programa" no momento pode se tornar apenas uma função ou método que faz parte de um programa maior. E essa função pode ser chamada várias vezes. E a memória que você deveria ter "liberado manualmente" será um vazamento de memória. É claro que isso é um julgamento.

gnasher729
fonte
2
isso parece meramente repetir o argumento apresentado (e muito melhor explicado) na resposta principal publicada algumas horas antes: "pode ​​ser muito mais fácil mover o código que usa a memória juntamente com a alocação e desalocação para um componente separado e usá-lo posteriormente em um contexto diferente, onde o tempo de uso da memória precisa ser controlado pelo usuário do componente ... "
gnat
1

Como é muito provável que em algum momento você queira alterar seu programa, talvez o integre a outra coisa, execute várias instâncias em sequência ou em paralelo. Então será necessário liberar manualmente essa memória - mas você não lembrará mais das circunstâncias e custará muito mais tempo para re-entender seu programa.

Faça as coisas enquanto sua compreensão sobre elas ainda estiver fresca.

É um pequeno investimento que pode render grandes retornos no futuro.

Agent_L
fonte
2
isso não parece acrescentar nada substancial aos pontos apresentados e explicados nas 11 respostas anteriores. Em particular, o argumento sobre a possibilidade de alterar o programa no futuro já foi feito três ou quatro vezes
gnat
11
@gnat De uma maneira complicada e obscurecida - sim. Em uma declaração clara - não. Vamos nos concentrar na qualidade da resposta, não na rapidez.
Agent_L
11
qualidade sábio, resposta superior parece ser muito melhor para explicar este ponto
mosquito
1

Não, não é necessário, mas é uma boa ideia.

Você diz que "coisas misteriosas e terríveis aconteceriam se eu não liberasse memória depois de terminar de usá-la".

Em termos técnicos, a única conseqüência disso é que seu programa continuará consumindo mais memória até que um limite rígido seja alcançado (por exemplo, seu espaço de endereço virtual esteja esgotado) ou o desempenho se torne inaceitável. Se o programa está prestes a sair, nada disso importa porque o processo efetivamente deixa de existir. As "coisas misteriosas e terríveis" são puramente sobre o estado mental do desenvolvedor. Encontrar a fonte de um vazamento de memória pode ser um pesadelo absoluto (isso é um eufemismo) e é preciso muita habilidade e disciplina para escrever um código sem vazamentos. A abordagem recomendada para o desenvolvimento dessa habilidade e disciplina é sempre liberar memória quando ela não for mais necessária, mesmo que o programa esteja prestes a terminar.

Obviamente, isso tem a vantagem adicional de que seu código possa ser reutilizado e adaptado com mais facilidade, como outros já disseram.

No entanto , há pelo menos um caso em que é melhor não liberar memória imediatamente antes do encerramento do programa.

Considere o caso em que você fez milhões de pequenas alocações e elas foram trocadas principalmente para o disco. Quando você começa a liberar tudo, a maior parte da sua memória precisa ser trocada novamente para a RAM, para que as informações da contabilidade possam ser acessadas, apenas para descartar imediatamente os dados. Isso pode fazer com que o programa demore alguns minutos apenas para sair! Sem mencionar colocar muita pressão no disco e na memória física durante esse período. Se a memória física é escassa para começar (talvez o programa esteja sendo fechado porque outro programa está consumindo muita memória), é possível que as páginas individuais precisem ser trocadas várias vezes quando vários objetos precisam ser liberados do mesma página, mas não são liberados consecutivamente.

Se o programa for interrompido, o sistema operacional simplesmente descartará toda a memória que foi trocada para o disco, o que é quase instantâneo, pois não requer nenhum acesso ao disco.

É importante observar que, em uma linguagem OO, chamar o destruidor de um objeto também forçará a troca de memória; se você precisar fazer isso, poderá liberar a memória.

Artelius
fonte
11
Você pode citar algo para apoiar a idéia de que a memória paginada precisa ser paginada para desalocar? Isso é obviamente ineficiente, certamente os sistemas operacionais modernos são mais inteligentes do que isso!
11
@ Jon of All Trades, sistemas operacionais modernos são inteligentes, mas ironicamente a maioria das linguagens modernas não é. Quando você liberta (algo), o compilador coloca "algo" na pilha e depois chama free (). Colocá-lo na pilha exige trocá-lo novamente para a RAM, se for trocado.
Jeffiekins
@ JonofAllTrades, uma lista livre teria esse efeito, embora seja uma implementação malloc bastante obsoleta. Mas o sistema operacional não pode impedir que você use essa implementação.
0

Os principais motivos para a limpeza manual são: é menos provável que você confunda um vazamento de memória genuíno com um bloco de memória que será limpo apenas na saída; às vezes, você encontrará bugs no alocador apenas quando percorrer toda a estrutura de dados para desalocar-lo, e você vai ter uma dor de cabeça muito menor se você refatorar para que algum objeto não precisa se desalocada antes de sair do programa.

Os principais motivos para não fazer a limpeza manual são: desempenho, desempenho, desempenho e a possibilidade de algum tipo de erro duas vezes livre ou usar após livre em seu código de limpeza desnecessário, que pode se transformar em um bug ou segurança de falha explorar.

Se você se preocupa com o desempenho, sempre deseja criar um perfil para descobrir onde está perdendo todo o seu tempo, em vez de adivinhar. Um possível comprometimento é agrupar seu código de limpeza opcional em um bloco condicional, deixá-lo na depuração para obter os benefícios de escrever e depurar o código e, em seguida, se e somente se você tiver determinado empiricamente que é demais sobrecarga, diga ao compilador para ignorá-lo no seu executável final. E um atraso no fechamento do programa é, quase por definição, quase nunca no caminho crítico.

Davislor
fonte