Pergunta do engenheiro de nível básico sobre gerenciamento de memória

9

Faz alguns meses desde que comecei minha posição como desenvolvedor de software de nível básico. Agora que já passei de algumas curvas de aprendizado (por exemplo, idioma, jargão, sintaxe de VB e C #), estou começando a me concentrar em tópicos mais esotéricos, para escrever um software melhor.

Uma pergunta simples que apresentei a um colega de trabalho foi respondida com "Estou focando nas coisas erradas". Embora eu respeite esse colega de trabalho, discordo que essa é uma "coisa errada" a ser focada.

Aqui estava o código (em VB) e seguido pela pergunta.

Nota: A função GenerateAlert () retorna um número inteiro.

Dim alertID as Integer = GenerateAlert()
_errorDictionary.Add(argErrorID, NewErrorInfo(Now(), alertID))    

vs ...

 _errorDictionary.Add(argErrorID, New ErrorInfo(Now(), GenerateAlert()))

Originalmente, escrevi o último e o reescrevi com o "Dim alertID", para que outra pessoa possa achar mais fácil ler. Mas aqui estava minha preocupação e pergunta:

Se alguém escrever isso com o Dim AlertID, de fato ocuparia mais memória; finito, mas mais, e esse método deveria ser chamado muitas vezes poderia levar a um problema? Como o .NET manipulará esse objeto AlertID. Fora do .NET, deve-se descartar manualmente o objeto após o uso (próximo ao final do sub).

Quero garantir que me torne um programador experiente que não dependa apenas da coleta de lixo. Estou pensando demais nisso? Estou me concentrando nas coisas erradas?

Sean Hobbs
fonte
11
Eu poderia facilmente afirmar que ele é 100%, já que a primeira versão é mais legível. Aposto que o compilador pode até cuidar do que você está preocupado. Mesmo se não, você está otimizando prematuramente.
Rig
6
Não tenho certeza de que ele realmente usaria mais memória com um número inteiro anônimo vs. um número inteiro nomeado. De qualquer forma, isso realmente é otimização prematura. Se você precisar se preocupar com a eficiência nesse nível (tenho quase certeza de que não), poderá precisar de C ++ em vez de C #. É bom entender os problemas de desempenho e o que acontece sob o capô, mas essa é uma pequena árvore em uma grande floresta.
Psr
5
O número inteiro nomeado vs anônimo não usaria mais memória, especialmente porque um número inteiro anônimo é apenas um número inteiro nomeado que VOCÊ não nomeou (o compilador ainda precisa nomear). No máximo, o número inteiro nomeado teria um escopo diferente e, portanto, poderia durar mais tempo. O número inteiro anônimo viveria apenas enquanto o método precisasse, o nomeado viveria enquanto o pai precisasse.
Joel Etherton
Vamos ver ... Se Integer for uma classe, ele será alocado no heap. A variável local (na pilha mais provável) manterá uma referência a ela. A referência será passada para o objeto errorDictionary. Se o tempo de execução estiver fazendo contagem de referência (ou tal), quando não houver mais referências, ele (o objeto) será desalocado do heap. Qualquer coisa na pilha é "desalocada" automaticamente quando o método sai. Se for um primitivo, (provavelmente) acabará na pilha.
Paul
Seu colega estava certo: o problema levantado por sua pergunta não deveria ter sido sobre otimização, mas sobre legibilidade .
21135 haylem

Respostas:

25

"A otimização prematura é a raiz de todos os males (ou pelo menos a maioria deles) na programação." - Donald Knuth

Quando se trata de sua primeira passagem, basta escrever seu código para que ele esteja correto e limpo. Se mais tarde for determinado que seu código é crítico para o desempenho (existem ferramentas para determinar isso chamado de criação de perfil), ele poderá ser reescrito. Se seu código não for considerado crítico para o desempenho, a legibilidade será muito mais importante.

Vale a pena investigar esses tópicos de desempenho e otimização? Absolutamente, mas não no dólar da sua empresa, se for desnecessário.

Christopher Berman
fonte
11
Em quem mais deve ser o dólar? Seu empregador se beneficia do aumento de suas habilidades e não mais do que você.
Marcin
Assuntos que não contribuem diretamente para sua tarefa atual? Você deve buscar essas coisas no seu próprio tempo. Se eu me sentasse e pesquisasse todos os itens do CompSci que despertaram minha curiosidade ao longo do dia, não faria nada. É para isso que servem minhas noites.
Christopher Berman
2
Esquisito. Alguns de nós têm uma vida pessoal e, como eu disse, o empregador se beneficia principalmente da pesquisa. A chave é realmente não passar o dia inteiro nela.
Marcin
6
Bom para você. Realmente não faz disso uma regra universal. Além disso, se você desencorajar seus funcionários de aprender no trabalho, tudo o que você fez foi desencorajá-los a aprender e os incentivou a encontrar outro empregador que realmente pague pelo desenvolvimento da equipe.
Marcin
2
Eu entendo as opiniões anotadas nos comentários acima; Gostaria de notar que perguntei durante o meu horário de almoço. :). Mais uma vez, obrigado a todos por sua contribuição aqui e em todo o site do Stack Exchange; é inestimável!
Sean Hobbs
5

Para o seu programa .NET médio, sim, é exagero. Pode haver momentos em que você deseje saber exatamente o que está acontecendo dentro do .NET, mas isso é relativamente raro.

Uma das transições difíceis que tive foi passar do C e do MASM para a programação no VB clássico nos anos 90. Eu estava acostumado a otimizar tudo para tamanho e velocidade. Eu tive que deixar esse pensamento para a maior parte e deixar o VB fazer as coisas para ser eficaz.

jfrankcarr
fonte
5

Como meu colega de trabalho sempre dizia:

  1. Faça funcionar
  2. Corrija todos os erros para que funcione perfeitamente
  3. Torná-lo sólido
  4. Aplique otimização se estiver executando lentamente

Em outras palavras, lembre-se sempre do KISS (mantenha-o estúpido). Devido ao excesso de engenharia, o excesso de pensamento em alguma lógica de código pode ser um problema para mudar de lógica na próxima vez. No entanto, manter o código limpo e simples é sempre uma boa prática .

No entanto, com o tempo e a experiência, você saberia melhor qual código cheira e precisaria de otimização em breve.

Yusubov
fonte
3

Deve-se escrever isso com o Dim AlertID

A legibilidade é importante. No seu exemplo, porém, eu não tenho certeza de que você está realmente fazendo as coisas que mais legível. GenerateAlert () tem um bom nome e não está adicionando muito ruído. Provavelmente há melhores usos do seu tempo.

de fato, ocuparia mais memória;

Eu suspeito que não. Essa é uma otimização relativamente direta para o compilador.

esse método deve ser chamado muitas vezes, pode levar a um problema?

O uso de uma variável local como intermediário não causa impacto no coletor de lixo. Se a memória de GenerateAlert () estiver nova, isso será importante. Mas isso será importante, independentemente da variável local ou não.

Como o .NET manipulará esse objeto AlertID.

AlertID não é um objeto. O resultado de GenerateAlert () é o objeto. AlertID é a variável, que se for uma variável local é simplesmente espaço associado ao método para acompanhar as coisas.

Fora do .NET, deve-se descartar manualmente o objeto após o uso

Essa é uma pergunta mais complexa que depende do contexto envolvido e da semântica de propriedade da instância fornecida por GenerateAlert (). Em geral, o que quer que tenha criado a instância deve excluí-la. Seu programa provavelmente pareceria significativamente diferente se fosse projetado com o gerenciamento manual de memória em mente.

Quero garantir que eu me torne um programador experiente que não apenas repasse a coleta de lixo. Estou pensando demais nisso? Estou me concentrando nas coisas erradas?

Um bom programador usa as ferramentas disponíveis, incluindo o coletor de lixo. É melhor pensar demais nas coisas do que viver alheio. Você pode estar se concentrando nas coisas erradas, mas, como estamos aqui, é melhor aprender sobre isso.

Telastyn
fonte
2

Faça o trabalho, faça-o limpo, faça-o SÓLIDO, ENTÃO faça-o funcionar tão rápido quanto ele precisa .

Essa deve ser a ordem normal das coisas. Sua primeira prioridade é fazer algo que passará nos testes de aceitação que eliminam os requisitos. Essa é sua primeira prioridade, porque é a primeira prioridade do seu cliente; atender aos requisitos funcionais dentro dos prazos de desenvolvimento. A próxima prioridade é escrever um código limpo e legível que seja fácil de entender e, portanto, possa ser mantido pela sua posteridade sem WTFs quando isso for necessário (quase nunca é uma questão de "se"; você ou alguém depois de você terá que ir voltar e mudar / consertar algo). A terceira prioridade é fazer o código aderir à metodologia SOLID (ou GRASP, se preferir), que coloca o código em blocos modulares, reutilizáveis ​​e substituíveis que novamente ajudam na manutenção (eles não apenas conseguem entender o que você fez e por que, mas há linhas limpas ao longo das quais posso remover e substituir cirurgicamente pedaços de código). A última prioridade é desempenho; se o código é importante o suficiente para atender às especificações de desempenho, é quase certamente importante o suficiente para ser corrigido, limpo e SOLID primeiro.

Fazendo eco a Christopher (e Donald Knuth), "a otimização prematura é a raiz de todo mal". Além disso, o tipo de otimização que você está considerando é menor (uma referência ao seu novo objeto será criada na pilha, seja um nome ou não no código-fonte) e de um tipo que pode não causar nenhuma diferença na compilação IL. Os nomes das variáveis ​​não são transportados para a IL, portanto, como você está declarando a variável antes do seu primeiro (e provavelmente apenas) uso, eu apostaria um pouco de dinheiro da cerveja em que a IL é idêntica entre seus dois exemplos. Então, seu colega de trabalho está 100% certo; você está procurando no lugar errado, se estiver procurando instanciação variável variável vs inline em busca de algo para otimizar.

As micro-otimizações no .NET quase nunca valem a pena (estou falando de 99,99% dos casos). Em C / C ++, talvez, se você souber o que está fazendo. Ao trabalhar em um ambiente .NET, você já está suficientemente longe do metal do hardware para que haja uma sobrecarga significativa na execução do código. Portanto, como você já está em um ambiente que indica que desistiu da velocidade alucinante e procura escrever código "correto", se algo em um ambiente .NET realmente não estiver funcionando rápido o suficiente, sua complexidade é muito alto, ou você deve considerar paralelizar isso. Aqui estão alguns indicadores básicos a serem seguidos para otimização; Garanto que sua produtividade em otimização (velocidade obtida pelo tempo gasto) aumentará rapidamente:

  • Alterar a forma da função é mais importante do que alterar os coeficientes - complexidade WRT Big-Oh, você pode reduzir pela metade o número de etapas que devem ser executadas em um algoritmo N 2 e ainda possui um algoritmo de complexidade quadrática, embora seja executado em metade do tempo que costumava. Se esse é o limite inferior da complexidade para esse tipo de problema, que seja, mas se houver uma solução NlogN, linear ou logarítmica para o mesmo problema, você obterá mais trocando algoritmos para reduzir a complexidade do que otimizando o que possui.
  • Só porque você não pode ver a complexidade, não significa que isso não está lhe custando - muitas das frases de efeito mais elegantes da palavra apresentam um desempenho terrível (por exemplo, o verificador principal Regex é uma função de complexidade exponencial, embora eficiente A avaliação principal envolvendo a divisão do número por todos os números primos menores que sua raiz quadrada é da ordem de O (Nlog (sqrt (N))) .O Linq é uma ótima biblioteca porque simplifica o código, mas diferente de um mecanismo SQL, o .Net O compilador não tentará encontrar a maneira mais eficiente de executar sua consulta.Você precisa saber o que acontecerá quando usar um método e, portanto, por que um método pode ser mais rápido se colocado mais cedo (ou mais tarde) na cadeia, enquanto produz os mesmos resultados.
  • OTOH, quase sempre existe uma troca entre complexidade de origem e complexidade de tempo de execução - o SelectionSort é muito fácil de implementar; você provavelmente poderia fazê-lo em 10LOC ou menos. MergeSort é um pouco mais complexo, Quicksort ainda mais e RadixSort ainda mais. Porém, à medida que os algoritmos aumentam a complexidade da codificação (e, portanto, o tempo de desenvolvimento "inicial"), eles diminuem a complexidade do tempo de execução; MergeSort e QuickSort são NlogN, e RadixSort é geralmente considerado linear (tecnicamente é NlogM, onde M é o maior número em N).
  • Quebre rápido - Se houver uma verificação que possa ser feita de forma barata, com probabilidade significativa de ser verdadeira e signifique que você pode seguir em frente, faça essa verificação primeiro. Se o seu algoritmo, por exemplo, se importa apenas com números que terminam em 1, 2 ou 3, o caso mais provável (dados completamente aleatórios) é um número que termina em outro dígito, portanto, teste se o número NÃO termina em 1, 2 ou 3, antes de fazer qualquer verificação para ver se o número termina em 1, 2 ou 3. Se uma parte da lógica exigir A&B e P (A) = 0,9 enquanto P (B) = 0,1, verifique B primeiro, a menos que! A então! B (como if(myObject != null && myObject.someProperty == 1)) ou B demore mais de 9 vezes mais que A para avaliar ( if(myObject != null && some10SecondMethodReturningBool())).
  • Não faça nenhuma pergunta para a qual você já saiba a resposta - Se você tiver uma série de condições de "falha" e uma ou mais dessas condições dependerem de uma condição mais simples que também deve ser verificada, nunca verifique as duas. estes independentemente. Por exemplo, se você tem uma verificação que requer A e uma verificação que requer A &&B, você deve marcar A e, se verdadeiro, deve verificar B. Se! A, então! A &&B, então nem se preocupe.
  • Quanto mais vezes você faz algo, mais deve prestar atenção em como isso é feito - esse é um tema comum no desenvolvimento, em muitos níveis; em um sentido geral de desenvolvimento, "se uma tarefa comum é demorada ou complicada, continue fazendo até que você esteja frustrado e com conhecimento suficiente para encontrar uma maneira melhor". Em termos de código, quanto mais vezes um algoritmo ineficiente for executado, mais você obterá um desempenho geral otimizando-o. Existem ferramentas de criação de perfil que podem pegar um assembly binário e seus símbolos de depuração e mostrar, depois de executar alguns casos de uso, quais linhas de código foram mais executadas. Essas linhas, e as linhas que as executam, são as que você deve prestar mais atenção, porque qualquer aumento na eficiência que você conseguir lá será multiplicado.
  • Um algoritmo mais complexo se parece com um algoritmo menos complexo se você jogar hardware suficiente nele . Há momentos em que você apenas precisa perceber que seu algoritmo está se aproximando dos limites técnicos do sistema (ou parte dele) em que o está executando; a partir desse ponto, se precisar ser mais rápido, você ganhará mais simplesmente executando-o em um hardware melhor. Isso também se aplica à paralelização; um algoritmo de complexidade N 2 , quando executado em N núcleos, parece linear. Portanto, se você tiver certeza de que atingiu o limite de menor complexidade para o tipo de algoritmo que está escrevendo, procure maneiras de "dividir e conquistar".
  • É rápido quando é rápido o suficiente - A menos que você faça a montagem da embalagem manual para atingir um chip específico, sempre há algo a ser ganho. No entanto, a menos que você queira montar manualmente a embalagem, você deve sempre ter em mente o que o cliente chamaria de "suficientemente bom". Novamente, "a otimização prematura é a raiz de todo mal"; quando o seu cliente liga rápido o suficiente, você termina até que ele não considere mais rápido o suficiente.
KeithS
fonte
0

O único momento para se preocupar com a otimização desde o início é quando você sabe que está lidando com algo que é enorme ou que você sabe que será executado inúmeras vezes.

A definição de "enorme" obviamente varia de acordo com a aparência de seus sistemas de destino.

Loren Pechtel
fonte
0

Eu preferiria a versão em duas linhas simplesmente porque é mais fácil avançar com um depurador. Uma linha com várias chamadas incorporadas torna mais difícil.

Slapout
fonte