Ao responder a essa pergunta , comecei a me perguntar por que tantos desenvolvedores acreditam que um bom design não deve levar em consideração o desempenho, pois isso afetaria a legibilidade e / ou a manutenção.
Acredito que um bom design também leva em consideração o desempenho no momento em que foi escrito e que um bom desenvolvedor com um bom design pode escrever um programa eficiente sem afetar adversamente a legibilidade ou a capacidade de manutenção.
Embora reconheça que existem casos extremos, por que muitos desenvolvedores insistem em que um programa / design eficiente resultará em baixa legibilidade e / ou baixa manutenção e, consequentemente, que o desempenho não deva ser considerado pelo design?
Respostas:
Eu acho que essas visões geralmente são reações a tentativas de (micro) otimização prematura , que ainda prevalecem e geralmente causam muito mais mal do que bem. Quando alguém tenta combater essas opiniões, é fácil cair - ou pelo menos parecer - o outro extremo.
Não obstante, é verdade que, com o enorme desenvolvimento de recursos de hardware nas últimas décadas, para a maioria dos programas criados atualmente, o desempenho deixou de ser um fator limitante importante. Obviamente, deve-se levar em consideração o desempenho esperado e alcançável durante a fase de design, a fim de identificar os casos em que o desempenho pode ser (venha) uma questão importante . E então é realmente importante projetar para o desempenho desde o início. No entanto, a simplicidade geral, a legibilidade e a manutenção são ainda mais importantes . Como outros observaram, o código otimizado para desempenho é mais complexo, mais difícil de ler e manter e mais propenso a erros do que a solução de trabalho mais simples. Assim, qualquer esforço despendido na otimização deve ser comprovada - não apenas acredita- trazer benefícios reais, ao mesmo tempo em que diminui o mínimo possível a manutenção do programa a longo prazo. Portanto, um bom design isola as partes complexas e com alto desempenho do restante do código , que é mantido o mais simples e limpo possível.
fonte
Chegando à sua pergunta do lado de um desenvolvedor que trabalha com código de alto desempenho, há várias coisas a considerar no design.
Faça certo, faça bonito, rápido. Naquela ordem.
fonte
contains
bastante, use aHashSet
, não aArrayList
. O desempenho pode não importar, mas não há razão para não fazer isso. Explore congruências entre bom design e desempenho - se estiver processando alguma coleção, tente fazer tudo em uma única passagem, que provavelmente será mais legível e mais rápida (provavelmente).Se eu puder presumir "emprestar" o belo diagrama do @ greengit e fazer uma pequena adição:
Todos nós fomos "ensinados" que existem curvas de troca. Além disso, todos assumimos que somos programadores tão ótimos que qualquer programa que escrevemos é tão rígido que fica na curva . Se um programa está na curva, qualquer melhoria em uma dimensão implica necessariamente um custo na outra dimensão.
Na minha experiência, os programas só chegam perto de qualquer curva ao serem ajustados, ajustados, martelados, encerados e, em geral, transformados em "código de golfe". A maioria dos programas tem muito espaço para melhorias em todas as dimensões. Aqui está o que eu quero dizer.
fonte
Precisamente porque os componentes de software de alto desempenho geralmente são ordens de magnitude mais complexas do que outros componentes de software (todas as outras coisas são iguais).
Mesmo assim, não é tão claro se as métricas de desempenho são um requisito extremamente importante, é imperativo que o design tenha complexidade para atender a esses requisitos. O perigo é um desenvolvedor que desperdiça um sprint em um recurso relativamente simples tentando extrair alguns milissegundos extras de seu componente.
Independentemente disso, a complexidade do design tem uma correlação direta com a capacidade de um desenvolvedor aprender e se familiarizar rapidamente com esse design, e outras modificações na funcionalidade de um componente complexo podem resultar em erros que podem não ser detectados pelos testes de unidade. Projetos complexos têm muito mais facetas e possíveis casos de teste a serem considerados, tornando o objetivo de 100% de cobertura de teste de unidade ainda mais um sonho.
Dito isso, deve-se notar que um componente de software com desempenho ruim pode ter um desempenho ruim apenas porque foi escrito de maneira tola e desnecessariamente complexa, com base na ignorância do autor original (fazendo 8 chamadas ao banco de dados para criar uma única entidade quando apenas uma delas o faria. , código completamente desnecessário que resulta em um único caminho de código, independentemente, etc ...) Esses casos são mais uma questão de melhorar a qualidade do código e os aumentos de desempenho que ocorrem como conseqüência do refator e NÃO necessariamente da consequência pretendida.
Assumindo um componente bem projetado, no entanto, sempre será menos complexo do que um componente igualmente bem projetado ajustado para desempenho (todas as outras coisas são iguais).
fonte
Não é tanto que essas coisas não possam coexistir. O problema é que o código de todos é lento, ilegível e insustentável na primeira iteração. O resto do tempo é gasto trabalhando para melhorar o que é mais importante. Se isso é desempenho, então vá em frente. Não escreva códigos maldosamente terríveis, mas se ele tiver que ser X rápido, faça-o X rápido. Acredito que desempenho e limpeza são basicamente não correlacionados. Código de desempenho não causa código feio. No entanto, se você gasta seu tempo ajustando cada código para ser rápido, adivinhe o que você não gastou? Tornando seu código limpo e sustentável.
fonte
Como você pode ver...
Portanto, desempenho e legibilidade são modestamente relacionados - e na maioria dos casos, não há grandes incentivos reais preferindo o primeiro ao invés do último. E eu estou falando aqui sobre linguagens de alto nível.
fonte
Na minha opinião, o desempenho deve ser considerado quando se trata de um problema real (ou por exemplo, um requisito). Não fazer isso tende a levar a microoptimizações, o que pode levar a um código mais ofuscado, apenas para economizar alguns microssegundos aqui e ali, o que, por sua vez, leva a um código menos sustentável e menos legível. Em vez disso, deve-se concentrar nos gargalos reais do sistema, se necessário , e enfatizar o desempenho no local.
fonte
O ponto não é a legibilidade, deve sempre superar a eficiência. Se você sabe desde o início que seu algoritmo precisa ser altamente eficiente, esse será um dos fatores que você usa para desenvolvê-lo.
A questão é que a maioria dos casos de uso não precisa de código rápido ofuscante. Em muitos casos, a interação de E / S ou do usuário causa muito mais atraso do que a execução do algoritmo. O ponto é que você não deve se esforçar para tornar algo mais eficiente se você não souber que é o gargalo da garrafa.
A otimização do código para desempenho geralmente o torna mais complicado, porque geralmente envolve fazer as coisas de maneira inteligente, em vez das mais intuitivas. Um código mais complicado é mais difícil de manter e mais difícil para outros desenvolvedores (ambos são custos que devem ser considerados). Ao mesmo tempo, os compiladores são muito bons em otimizar casos comuns. É possível que sua tentativa de melhorar um caso comum signifique que o compilador não reconheça mais o padrão e, portanto, não possa ajudá-lo a tornar seu código rápido. Note-se que isso não significa escrever o que você quiser, sem se preocupar com o desempenho. Você não deve estar fazendo nada que seja claramente ineficiente.
O objetivo é não se preocupar com pequenas coisas que possam melhorar as coisas. Use um criador de perfil e veja que 1) o que você tem agora é um problema e 2) o que você mudou foi uma melhoria.
fonte
Eu acho que a maioria dos programadores sente essa sensação simplesmente porque, na maioria das vezes, o código de desempenho é um código baseado em muito mais informações (sobre o contexto, conhecimento de hardware, arquitetura global) do que qualquer outro código em aplicativos. A maioria dos códigos expressará apenas algumas soluções para problemas específicos que são encapsulados em algumas abstrações de maneira modular (funções semelhantes) e isso significa limitar o conhecimento do contexto apenas ao que entra nesse encapsulamento (como parâmetros de função).
Quando você escreve para alto desempenho, depois de corrigir qualquer fertilização algorítmica, entra em detalhes que exigem muito mais conhecimento sobre o contexto. Isso pode sobrecarregar naturalmente qualquer programador que não se sinta suficientemente focado para a tarefa.
fonte
Como o custo do aquecimento global (desses ciclos extras de CPU dimensionados em centenas de milhões de PCs, além de instalações massivas de data center) e da duração medíocre da bateria (nos dispositivos móveis do usuário), conforme necessário para executar seu código mal otimizado, raramente aparece na maioria desempenho do programador ou revisões por pares.
É uma externalidade econômica negativa, semelhante a uma forma de poluição ignorada. Portanto, a relação custo / benefício de se pensar em desempenho é distorcida mentalmente da realidade.
Os designers de hardware têm trabalhado duro para adicionar recursos de economia de energia e escala de clock às CPUs mais recentes. Cabe aos programadores permitir que o hardware aproveite esses recursos com mais frequência, não consumindo todos os ciclos de clock da CPU disponíveis.
ADICIONADO: Nos tempos antigos, o custo de um computador era de milhões, portanto, otimizar o tempo da CPU era muito importante. Então, o custo de desenvolvimento e manutenção do código tornou-se maior que o custo dos computadores; portanto, a otimização ficou fora de moda em comparação à produtividade do programador. Agora, no entanto, outro custo está se tornando maior que o custo dos computadores, o custo de alimentar e resfriar todos esses data centers está se tornando maior que o custo de todos os processadores internos.
fonte
Eu acho que é difícil conseguir todos os três. Eu acho que dois é possível. Por exemplo, acho que é possível obter eficiência e legibilidade em alguns casos, mas a manutenção pode ser difícil com o código micro-ajustado. O código mais eficiente do planeta geralmente não têm tanto de manutenção e legibilidade como é provavelmente óbvio para a maioria, a menos que você é o tipo que pode compreender a mão SoA-vetorizado, multithreaded código SIMD que a Intel escreve com inline montagem, ou o mais corte algoritmos de borda usados no setor com artigos matemáticos de 40 páginas publicados há apenas 2 meses e 12 bibliotecas no valor de código para uma estrutura de dados incrivelmente complexa.
Microeficiência
Uma coisa que eu sugiro que possa ser contrária à opinião popular é que o código algorítmico mais inteligente é geralmente mais difícil de manter do que o algoritmo direto mais micro-ajustado. Essa ideia de que as melhorias de escalabilidade rendem mais dinheiro do que o código micro-ajustado (por exemplo: padrões de acesso amigáveis ao cache, multithreading, SIMD etc.) é algo que eu desafiaria, pelo menos por ter trabalhado em um setor cheio de recursos extremamente complexos. estruturas e algoritmos de dados (a indústria de efeitos visuais), especialmente em áreas como processamento de malha, porque o estrondo pode ser grande, mas o dinheiro é extremamente caro quando você introduz novos algoritmos e estruturas de dados que ninguém nunca ouviu falar, uma vez que é marca Novo. Além disso, eu
Portanto, essa idéia de que otimizações algorítmicas sempre superam, digamos, otimizações relacionadas a padrões de acesso à memória é sempre algo que eu não concordo totalmente. É claro que se você estiver usando um tipo de bolha, nenhuma micro-otimização pode ajudá-lo lá ... mas dentro da razão, eu não acho que seja sempre tão clara. E, sem dúvida, as otimizações algorítmicas são mais difíceis de manter do que as micro otimizações. Eu acho muito mais fácil manter, digamos, o Embree da Intel, que pega um algoritmo BVH clássico e direto e apenas ajusta a porcaria do que o código OpenVDB da Dreamwork para formas avançadas de acelerar algoritmos a simulação de fluidos. Então, no meu setor, pelo menos, eu gostaria de ver mais pessoas familiarizadas com a arquitetura de computadores otimizando mais, como a Intel fez quando entrou em cena, em vez de criar milhares e milhares de novos algoritmos e estruturas de dados. Com micro otimizações eficazes, as pessoas podem encontrar cada vez menos razões para inventar novos algoritmos.
Eu trabalhei em uma base de código legada antes em que quase todas as operações de usuário tinham sua própria estrutura de dados e algoritmo por trás (adicionando centenas de estruturas de dados exóticas). E a maioria deles tinha características de desempenho muito distorcidas, sendo muito restrita. Teria sido muito mais fácil se o sistema pudesse girar em torno de algumas dúzias de estruturas de dados mais amplamente aplicáveis, e acho que poderia ter sido o caso se elas fossem micro-otimizadas muito melhor. Mencionei este caso porque a micro-otimização pode potencialmente melhorar a capacidade de manutenção tremendamente nesse caso, se isso significa a diferença entre centenas de estruturas de dados micro-pessimizadas que nem sequer podem ser usadas com segurança para finalidades restritas de leitura que envolvem falhas de cache deixadas e certo vs.
Idiomas funcionais
Enquanto isso, alguns dos códigos mais sustentáveis que eu já encontrei foram razoavelmente eficientes, mas extremamente difíceis de ler, pois foram escritos em linguagens funcionais. Em geral, a legibilidade e a uber manutenibilidade são idéias conflitantes na minha opinião.
É realmente difícil tornar o código legível, sustentável e eficiente ao mesmo tempo. Normalmente, você precisa comprometer um pouco um desses três, se não dois, como comprometer a legibilidade para manutenção ou comprometer a manutenção para obter eficiência. Geralmente é a manutenção que sofre quando você procura muitos dos outros dois.
Legibilidade vs. Manutenção
Agora, como dito, acredito que legibilidade e manutenção não são conceitos harmoniosos. Afinal, o código mais legível para a maioria de nós, os mortais, mapeia de maneira muito intuitiva os padrões de pensamento humanos, e os padrões de pensamentos humanos são inerentemente propensos a erros: " Se isso acontecer, faça isso. Se isso acontecer, faça isso. Caso contrário, faça isso. , Esqueci uma coisa! Se esses sistemas interagem entre si, isso deve acontecer para que esse sistema possa fazer isso ... oh espere, e o sistema quando esse evento é acionado?"Esqueci a citação exata, mas alguém disse uma vez que se Roma fosse construída como software, seria necessário um pouso de pássaro em uma parede para derrubá-la. É o caso da maioria dos softwares. É mais frágil do que costumamos gostar. Algumas linhas de código aparentemente inócuo aqui e ali podem parar a ponto de nos fazer reconsiderar todo o design, e linguagens de alto nível que visam ser o mais legíveis possível não são exceções a esses erros de design humano .
As linguagens funcionais puras são quase tão invulneráveis quanto se pode viabilizar (nem mesmo perto de invulneráveis, mas relativamente muito mais próximas do que a maioria). E isso é parcialmente porque eles não mapeiam intuitivamente o pensamento humano. Eles não são legíveis. Eles nos impõem padrões de pensamento que nos levam a resolver problemas com o mínimo de casos especiais possível, usando a quantidade mínima de conhecimento possível e sem causar efeitos colaterais. Eles são extremamente ortogonais, permitem que o código seja frequentemente alterado e alterado sem surpresas tão épicas que precisamos repensar o design em uma prancheta, até ao ponto de mudar de idéia sobre o design geral, sem reescrever tudo. Não parece mais fácil manter isso do que isso ... mas o código ainda é muito difícil de ler,
fonte
Um problema é que o tempo finito do desenvolvedor significa que tudo o que você procura otimizar perde tempo com outras questões.
Existe um experimento bastante bom sobre isso mencionado no Código Completo de Meyer. Solicitou-se que diferentes grupos de desenvolvedores otimizassem velocidade, uso de memória, legibilidade, robustez e assim por diante. Constatou-se que seus projetos obtiveram uma pontuação alta no que quer que fossem solicitados a otimizar, mas inferiores em todas as outras qualidades.
fonte
Porque programadores experientes aprenderam que é verdade.
Trabalhamos com código que é enxuto e mesquinho e não apresenta problemas de desempenho.
Nós trabalhamos em muitos códigos que, para resolver problemas de desempenho, são MUITO complexos.
Um exemplo imediato que vem à mente é que meu último projeto incluiu 8.192 tabelas SQL fragmentadas manualmente. Isso foi necessário devido a problemas de desempenho. A configuração para selecionar uma tabela é muito mais simples do que selecionar e manter 8.192 shards.
fonte
Também existem algumas peças famosas de código altamente otimizado que dobrarão o cérebro da maioria das pessoas que suportam o caso de que código altamente otimizado é difícil de ler e entender.
Aqui está o mais famoso que eu acho. Retirado da Arena Quake III e atribuído a John Carmak, embora eu ache que houve várias iterações dessa função e ela não foi originalmente criada por ele (a Wikipedia não é ótima? ).
fonte