Descartando corretamente os objetos na finalização do servidor

9

Estou trabalhando em um grande projeto C ++. Consiste em um servidor que expõe uma API REST, fornecendo uma interface simples e amigável para um sistema muito amplo, que compreende muitos outros servidores. A base de código é bastante grande e complexa e evoluiu ao longo do tempo sem um design adequado inicial. Minha tarefa é implementar novos recursos e refatorar / corrigir o código antigo para torná-lo mais estável e confiável.

No momento, o servidor cria vários objetos de vida longa que nunca são finalizados nem descartados quando o processo termina. Isso torna o Valgrind quase inutilizável para a detecção de vazamentos, pois é impossível distinguir entre os milhares de (questionavelmente) vazamentos legítimos dos "perigosos".

Minha idéia é garantir que todos os objetos sejam descartados antes do término, mas quando eu fiz essa proposta, meus colegas e meu chefe se opuseram a mim, apontando que o sistema operacional liberaria essa memória de qualquer maneira (o que é óbvio para todos) e descartaria os objetos. desacelerará o desligamento do servidor (que, no momento, é basicamente uma chamada para std::exit). Respondi que ter um procedimento de desligamento "limpo" não implica necessariamente que se deva usá-lo. Sempre podemos ligar std::quick_exitou apenas kill -9o processo, se nos sentimos impacientes.

Eles responderam que "a maioria dos daemons e processos do Linux não se preocupam em liberar memória no desligamento". Embora eu possa ver isso, também é verdade que nosso projeto precisa de depuração de memória precisa, pois eu já encontrei corrupção de memória, liberações duplas e variáveis ​​não inicializadas.

Quais são seus pensamentos? Estou perseguindo um esforço inútil? Caso contrário, como convencer meus colegas e meu chefe? Se sim, por quê e o que devo fazer?

Marty
fonte
Além do argumento de desempenho (que é razoável!), É muito difícil isolar os objetos de vida longa e adicionar código de limpeza para eles?
Doc Brown

Respostas:

7

Adicione um comutador ao processo do servidor que possa ser usado durante as medições do valgrind que liberarão toda a memória. Você pode usar essa opção para testar. O impacto será mínimo durante as operações normais.

Tivemos um longo processo de execução que levaria alguns minutos para liberar milhares de objetos. Era muito mais eficiente simplesmente sair e deixá-los morrer. Infelizmente, como você indica, isso dificultou a detecção de verdadeiros vazamentos de memória com o valgrind ou qualquer outra ferramenta.

Esse foi um bom compromisso para nossos testes, sem afetar o desempenho normal.

Bill Door
fonte
11
+1 Pragmatismo FTW. Há valor na medição, mas também há valor no desligamento rápido.
Ross Patterson
2
Como alternativa a uma opção de linha de comando, considere também implementar a exclusão dos objetos permanentes dentro de um bloco #ifdef DEBUG.
Jules
3

A chave aqui é esta:

Embora eu possa ver isso, também é verdade que nosso projeto precisa de depuração de memória precisa, pois eu já encontrei corrupção de memória, liberações duplas e variáveis ​​não inicializadas.

Isso implica diretamente que a sua base de código é composta de nada além de esperança e string. Programadores competentes de C ++ não têm liberações duplas.

Você está absolutamente buscando um esforço inútil - como está, está abordando um pequeno sintoma do problema real: seu código é tão confiável quanto o módulo de serviço Apollo 13.

Se você programar seu servidor corretamente com RAII, esses problemas não ocorrerão e o problema em sua pergunta será eliminado. Além disso, seu código pode realmente ser executado corretamente de tempos em tempos. Assim, é claramente a melhor opção.

DeadMG
fonte
Certamente, o problema está em uma imagem maior. No entanto, é muito raro encontrar recursos e permissão para refatorar / reescrever um projeto para melhorar sua forma.
Cengiz pode dec
@CengizCan: Se você deseja corrigir erros, é necessário refatorar. É assim que funciona.
DeadMG
2

Uma boa abordagem seria restringir a discussão com seus colegas por meio de classificação. Dada uma grande base de código, certamente não há uma única razão, mas várias razões (identificáveis) para objetos de vida longa.

Exemplos:

  • Objetos de vida longa que não são referenciados por ninguém (vazamentos reais). É um erro de lógica de programação. Corrija aqueles com prioridade mais baixa, A menos que eles sejam responsáveis ​​pelo aumento da área de memória ao longo do tempo (e deteriorem a qualidade dos aplicativos). Se eles aumentarem o espaço ocupado pela memória, corrija-os com maior prioridade.

  • Objetos de vida longa, que ainda são referenciados, mas não são mais usados ​​(devido à lógica do programa), mas que não aumentam sua pegada de memória. Revise o código e tente encontrar os outros erros que levam a isso. Adicione comentários à base de código se for uma otimização intencional (desempenho).

  • Objetos de vida longa "por design". Padrão Singleton, por exemplo. Eles são realmente difíceis de se livrar, especialmente se for um aplicativo multiencadeado.

  • Objetos reciclados. Objetos de vida longa nem sempre precisam ser ruins. Eles também podem ser benéficos. Em vez de ter alocações / desalocações de memória de alta frequência, adicionar objetos atualmente não utilizados a um contêiner para desenhar quando esse bloco de memória for necessário novamente pode ajudar a acelerar um aplicativo e evitar a fragmentação de heap. Eles devem ser fáceis de liberar no momento do desligamento, talvez em uma compilação especial "instrumentação / verificada".

  • "objetos compartilhados" - objetos que são usados ​​(referenciados) por vários outros objetos e ninguém sabe exatamente quando é salvo para liberá-los. Considere transformá-los em objetos contados de referência.

Depois de classificar os motivos reais desses objetos não-passados, é muito mais fácil entrar em uma discussão caso a caso e encontrar uma resolução.

BitTickler
fonte
0

IMHO, a vida útil desses objetos nunca deve ser feita e deixada para morrer quando o sistema for desligado. Isso cheira a variáveis ​​globais, que todos sabemos como ruins, ruins, ruins, ruins. Especialmente na era dos ponteiros inteligentes, não há razão para fazer isso além da preguiça. Mais importante, porém, ele adiciona um nível de dívida técnica ao seu sistema com o qual alguém pode ter que lidar algum dia.

A idéia de "dívida técnica" é que, quando você usa um atalho como esse, quando alguém deseja alterar o código no futuro (digamos, eu quero poder fazer o cliente entrar no "modo offline" ou "modo de suspensão) ", ou quero poder trocar de servidor sem reiniciar o processo), eles terão que se esforçar para fazer o que você está deixando de fazer. Mas eles manterão seu código, para que não saibam tanto sobre você quanto você, e isso levará muito mais tempo (não estou falando 20% mais, estou falando 20x mais!). Mesmo se for você, você não trabalhará nesse código em particular por semanas ou meses, e levará muito mais tempo para tirar o pó das teias de aranha para implementá-lo corretamente.

Nesse caso, parece que você tem um acoplamento muito rígido entre o objeto do servidor e os objetos "de longa duração" ... há momentos em que um registro pode (e deve) sobreviver à conexão com o servidor. Manter todas as alterações feitas em um objeto em um servidor pode ser proibitivamente cara, por isso é melhor que os objetos gerados sejam realmente manipulados pelo objeto servidor, com chamadas de salvamento e atualização que realmente alteram o servidor. Isso geralmente é chamado de padrão de registro ativo. Vejo:

http://en.wikipedia.org/wiki/Active_record_pattern

No C ++, cada registro ativo teria um fraco_ptr no servidor, o que poderia gerar coisas de maneira inteligente se a conexão do servidor ficar escura. Essas classes podem ser preguiçosamente ou preenchidas em lote, dependendo de suas necessidades, mas a vida útil desses objetos deve estar apenas onde são usados.

Veja também:

É perda de tempo liberar recursos antes de sair de um processo?

O outro

IdeaHat
fonte
This reeks of global variablesComo você passa de "existem milhares de objetos que precisam ser liberados" para "eles devem ser globais"? Isso é um grande salto de lógica.
Doval
0

Se você puder identificar facilmente onde os objetos que devem sobreviver indefinidamente estão alocados, uma vez que seria possível alocá-los usando um mecanismo de alocação alternativo, para que eles não apareçam em um relatório de vazamento de valgrind ou apenas pareçam ser uma alocação única.

Caso você não esteja familiarizado com a ideia, aqui está um artigo sobre como impotir a alocação de memória personalizada em c ++ , embora observe que sua solução pode ser mais simples que os exemplos desse artigo, pois você não precisa lidar com a exclusão. !

Jules
fonte