Por que tantos desenvolvedores acreditam que desempenho, legibilidade e capacidade de manutenção não podem coexistir?


34

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?


9
Seria quase impossível argumentar sobre isso em larga escala, mas para pequenos pedaços de código é bastante óbvio. Basta comparar as versões legíveis e eficientes do, digamos, quicksort.
SK-logic

7
Mu. Você deve começar apoiando sua afirmação de que muitos desenvolvedores insistem que a eficiência leva à insustentabilidade.
Peter Taylor

2
SK-logic: Na minha opinião, essa é uma das melhores partes de todos os sites de troca de pilhas, uma vez que se questiona o óbvio, que pode ser saudável de vez em quando. O que pode ser óbvio para você pode não ser óbvio para outra pessoa e vice-versa. :) Compartilhar é se importar.
Andreas Johansson

2
@ Justin, não. Essa discussão parece-me pressupor uma situação em que há uma escolha forçada entre código eficiente ou código sustentável. O interlocutor não diz com que frequência ele se encontra nessa situação, e os respondentes não parecem alegar estar nessa situação com frequência.
Peter Taylor

2
-1 para a pergunta. Quando o li, pensei que era um homem de palha despejar a única resposta verdadeira: "Porque eles não usam python".
Ingo

Respostas:


38

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.


8
"Quando alguém tenta se opor a essas opiniões, é fácil cair - ou pelo menos parecer - o outro extremo". Eu tenho problemas o tempo todo com as pessoas que pensam que tenho uma visão oposta quando estou apenas equilibrando os profissionais com os contras. Não apenas na programação, em tudo.
Jhocking

1
Eu estou tão cansado de todos a discutir sobre isso que eu fico com raiva e tomar extremos ..
Thomas Bonini

Houve várias boas respostas, mas acho que a sua fez a melhor tentativa de detalhar as origens dessa mentalidade. Obrigado a todos os envolvidos!
justin

Minha resposta ... a maioria dos desenvolvedores são ruins em seus trabalhos
TheCatWhisperer

38

Chegando à sua pergunta do lado de um desenvolvedor que trabalha com código de alto desempenho, há várias coisas a considerar no design.

  • Não pessimize prematuramente. Quando você tiver a escolha entre dois projetos com complexidade igual, escolha aquele que tenha as melhores características de desempenho. Um dos famosos exemplos de C ++ é a prevalência de pós-incremento de contadores (ou iteradores) em loops. Esta é uma pessimização prematura totalmente desnecessária que PODE não custar nada, mas PODE, portanto, não faça isso.
  • Em muitos casos, você ainda não tem negócios para se aproximar da micro-otimização. As otimizações algorítmicas são um fruto mais baixo e quase sempre são muito mais fáceis de entender do que as otimizações realmente de baixo nível.
  • Se, e APENAS, se o desempenho for absolutamente crítico, você ficará deprimido. Na verdade, você isola o código o máximo que pode primeiro e, em seguida, fica suja. E fica muito sujo lá, com esquemas de cache, avaliação lenta, otimização de layout de memória para cache, blocos de intrínseca ou montagem em linha, camada sobre camada de modelos, etc. Você testa e documenta como loucos aqui, sabe que está indo ferir se você precisar fazer alguma manutenção neste código, mas é necessário, porque o desempenho é absolutamente crítico. Edit: A propósito, não estou dizendo que esse código não pode ser bonito, e deve ser feito o mais bonito possível, mas ainda será muito complexo e muitas vezes complicado em comparação com o código menos otimizado.

Faça certo, faça bonito, rápido. Naquela ordem.


Gosto da regra de ouro: 'fique bonito, rápido'. Naquela ordem'. Eu vou começar a usar isso.
Martin York

Exatamente. E isole o código no terceiro ponto, tanto quanto possível. Porque quando você muda para um hardware diferente, mesmo algo tão pequeno quanto um processador com um tamanho de cache diferente, essas coisas podem mudar.
precisa saber é o seguinte

@ KeithB - ​​você faz um bom argumento, vou adicioná-lo à minha resposta.
Joris Timmermans

+1: "Faça o que é certo, faça bonito, rápido. Nessa ordem". Resumo muito bom, com o qual concordo 90%. Às vezes, só consigo consertar certos bugs (acertar) depois que ficar bonito (e mais compreensível).
Giorgio

+1 em "Não pessimize prematuramente". O conselho para evitar a otimização prematura não é a permissão para empregar arbitrariamente algoritmos de cabeça para baixo. Se você estiver escrevendo Java e tiver uma coleção que estará chamando containsbastante, use a HashSet, não a ArrayList. 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).
Tom Anderson

16

Se eu puder presumir "emprestar" o belo diagrama do @ greengit e fazer uma pequena adição:

|
P
E
R
F
O  *               X <- a program as first written
R   * 
M    *
A      *
N        *
C          *  *   *  *  *
E
|
O -- R E A D A B I L I T Y --

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.


Pessoalmente, acho que há outro fim na curva em que ela sobe novamente no lado direito (desde que você se mova o suficiente para a direita (o que provavelmente significa repensar seu algoritmo)).
Martin York

2
+1 em "A maioria dos programas tem muito espaço para melhorias em todas as dimensões".
Steven

5

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


3

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.


2
    |
    P
    E
    R
    F
    O *
    R * 
    M *
    UMA *
    N *
    C * * * * *
    E
    |
    O - LEIABILIDADE -

Como você pode ver...

  • Sacrificar a legibilidade pode aumentar o desempenho - mas apenas muito. Após um certo ponto, você precisa recorrer a meios "reais", como algoritmos e hardware melhores.
  • Além disso, a perda de desempenho com o custo da legibilidade pode acontecer apenas em certa medida. Depois disso, você pode tornar seu programa o mais legível que desejar, sem afetar o desempenho. Por exemplo, adicionar comentários mais úteis não afeta o desempenho.

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.


1

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.


1

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.


1

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.


1

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.


Além da pergunta, se os PCs contribuíram para o aquecimento global, mesmo que fossem reais: é uma falácia que mais eficiência energética leve a menos demanda de energia. Quase o oposto é verdadeiro, como pode ser visto desde o primeiro dia em que um PC apareceu no mercado. Antes disso, algumas centenas ou milhares de Mainframe (cada um virtualmente equipado com sua própria usina) usavam muito menos energia do que hoje, onde 1 minuto de CPU calcula muito mais do que então em uma fração do custo e da demanda de energia. No entanto, a demanda total de energia para computação é maior do que antes.
Ingo

1

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,


1
"Microeficiência" é como dizer "Não existe acesso à memória O (1)"
Caleth 13/17/17

0

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.


Obviamente, você pode dedicar mais tempo, mas eventualmente você começar a questionar por que os desenvolvedores iria tirar uma folga emacs programação para expressar o amor para seus filhos, e nesse ponto você está basicamente Sheldon do Big Bang Theory
deworde

0

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.


0

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

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //      y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}
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.