A mudança gradual na metodologia de escrever código afetou o desempenho do sistema? E eu deveria me importar?

35

TD; DR:

Havia alguma confusão sobre o que eu estava perguntando, então aqui está a ideia principal por trás da pergunta:

Eu sempre pretendi que a pergunta fosse o que é. Eu posso não ter articulado bem originalmente. Mas a intenção sempre foi " é um código modular, separado, acoplado, desacoplado, refatorado " marcadamente mais lento por sua própria natureza do que o código " monolítico de unidade única, faça tudo em um só lugar, um arquivo, código fortemente acoplado ". O resto são apenas detalhes e várias manifestações disso que me deparei na época, agora ou depois. É mais lento, com certeza em alguma escala. Como um disco sem desfragmentação, você deve coletar as peças de qualquer lugar. É mais lento. Com certeza. Mas eu deveria me importar?

E a questão não é sobre ...

não se trata de micro-otimização, otimização prematura, etc. Não se trata de "otimizar isto ou aquela parte até a morte".

Então o que é?

Trata-se da metodologia e técnicas gerais e maneiras de pensar sobre a escrita de código que surgiu ao longo do tempo:

  • "injete esse código na sua classe como uma dependência"
  • "escreve um arquivo por classe"
  • "separa sua visão do seu banco de dados, controlador, domínio".
  • não escreva espaguetes homogêneos com código único, mas escreva muitos componentes modulares separados que trabalham juntos

Trata-se da maneira e do estilo de código que é atualmente - nesta década - visto e defendido na maioria das estruturas, defendido em convenções, transmitido pela comunidade. É uma mudança de pensamento de 'blocos monolíticos' para 'microsserviços'. E com isso vem o preço em termos de desempenho e sobrecarga no nível da máquina, e também algumas sobrecargas no nível do programador.

Segue a pergunta original:

No campo da Ciência da Computação, notei uma mudança notável no pensamento quando se trata de programação. Me deparo com o conselho muitas vezes que é assim:

  • escreva um código menor em função de função (mais testável e sustentável dessa maneira)
  • refatorar o código existente em partes cada vez menores até que a maioria dos métodos / funções tenha apenas algumas linhas e fique claro qual é o objetivo delas (o que cria mais funções, comparado a um bloco monolítico maior)
  • escrever funções que fazem apenas uma coisa - separação de preocupações, etc (que geralmente cria mais funções e mais quadros em uma pilha)
  • crie mais arquivos (uma classe por arquivo, mais classes para fins de decomposição, para fins de camada, como MVC, arquitetura de domínio, padrões de design, OO, etc, o que cria mais chamadas do sistema de arquivos)

Essa é uma alteração comparada às práticas de codificação "antigas" ou "desatualizadas" ou "espaguete", nas quais você tem métodos abrangendo 2500 linhas, e grandes classes e objetos divinos fazendo tudo.

Minha pergunta é esta:

quando se trata de código de máquina, 1s e 0s, instruções de montagem, pratos HDD, devo me preocupar com o fato de que meu código OO perfeitamente separado por classe e com várias funções e métodos refatorados pequenos a minúsculos também gera sobrecarga extra?

Detalhes

Embora eu não esteja intimamente familiarizado com o modo como o código OO e suas chamadas de método são tratadas no ASM no final, e como as chamadas de DB e as chamadas de compilador se traduzem em mover o braço do atuador em uma bandeja de disco rígido, tenho alguma ideia. Suponho que cada chamada de função extra, chamada de objeto ou chamada "#include" (em alguns idiomas) gere um conjunto extra de instruções, aumentando assim o volume do código e adicionando várias despesas gerais de "fiação de código", sem adicionar o código "útil" real . Também imagino que boas otimizações podem ser feitas no ASM antes de serem executadas no hardware, mas essa otimização pode fazer muito também.

Portanto, minha pergunta - quanto de sobrecarga (em espaço e velocidade) um código bem separado (código que é dividido em centenas de arquivos, classes e padrões de design etc.) realmente se compara a um "grande método que contém tudo em um arquivo monolítico ", devido a essa sobrecarga?

ATUALIZAÇÃO para maior clareza:

Estou assumindo que pegar o mesmo código e dividi-lo, refatorá-lo, separá-lo em mais e mais funções, objetos e métodos e classes resultará em mais e mais parâmetros passando entre pedaços de código menores. Porque, com certeza, o código de refatoração deve manter o encadeamento, e isso exige a passagem de parâmetros. Mais métodos ou mais classes ou mais padrões de design dos Métodos de Fábrica resultam em mais sobrecarga na transmissão de vários bits de informação mais do que no caso de uma única classe ou método monolítico.

Foi dito em algum lugar (citação TBD) que até 70% de todo o código é constituído pela instrução MOV do ASM - carregando os registros da CPU com variáveis ​​apropriadas, não a computação real sendo feita. No meu caso, você carrega o tempo da CPU com instruções PUSH / POP para fornecer ligação e passagem de parâmetros entre várias partes do código. Quanto menor você criar suas partes de código, mais "ligação" será necessária. Estou preocupado com o fato de esse vínculo aumentar o inchaço e a desaceleração do software, e me pergunto se devo me preocupar com isso e quanto, se é que existe, porque as gerações atuais e futuras de programadores que estão construindo software para o próximo século , terá que conviver e consumir software criado com essas práticas.

UPDATE: vários arquivos

Estou escrevendo um novo código agora que está substituindo lentamente o código antigo. Em particular, observei que uma das classes antigas era um arquivo de linha ~ 3000 (como mencionado anteriormente). Agora está se tornando um conjunto de 15 a 20 arquivos localizados em vários diretórios, incluindo arquivos de teste e não incluindo a estrutura PHP que estou usando para vincular algumas coisas. Mais arquivos estão chegando também. Quando se trata de E / S de disco, o carregamento de vários arquivos é mais lento que o carregamento de um arquivo grande. É claro que nem todos os arquivos são carregados, eles são carregados conforme necessário, e existem opções de armazenamento em cache em disco e memória e, ainda assim, acredito que seja loading multiple filesnecessário mais processamento do que loading a single filena memória. Estou acrescentando isso à minha preocupação.

ATUALIZAÇÃO: Dependência Injete tudo

Voltando a isso depois de um tempo ... Acho que minha pergunta foi mal compreendida. Ou talvez eu tenha escolhido não entender algumas respostas. Não estou falando sobre micro-otimização, como algumas respostas destacaram (pelo menos eu acho que chamar o que estou falando sobre micro-otimização é um termo impróprio), mas sobre o movimento de "Refatorar código para afrouxar o acoplamento rígido", como um todo , em todos os níveis do código. Vim de Zend Con recentemente, onde esse estilo de código tem sido um dos pontos centrais e centrais da convenção. Desacoplar a lógica da exibição, exibição do modelo, modelo do banco de dados e, se possível, desacoplar dados do banco de dados. Dependency-Injete tudo, o que às vezes significa apenas adicionar código de fiação (funções, classes, clichê) que não faz nada, mas serve como ponto de costura / gancho, dobrando facilmente o tamanho do código na maioria dos casos.

ATUALIZAÇÃO 2: "Separar código em mais arquivos" afeta significativamente o desempenho (em todos os níveis da computação)

Como a filosofia do compartmentalize your code into multiple filesimpacto afeta a computação atual (desempenho, utilização de disco, gerenciamento de memória, tarefas de processamento da CPU)?

Eu estou falando sobre

Antes...

Em um passado hipotético, mas bastante real e não tão distante, você pode escrever facilmente um monobloco de um arquivo que possui model e view e controller spaghetti ou não-spaghetti-coded, mas que executa tudo quando já está carregado. Fazendo alguns benchmarks no passado usando o código C, descobri que é MUITO mais rápido carregar um único arquivo de 900Mb na memória e processá-lo em grandes pedaços do que carregar um monte de arquivos menores e processá-los em uma refeição pacífica menor pedaços fazendo o mesmo trabalho no final.

.. E agora*

Hoje eu me pego olhando para o código que mostra um razão, que possui recursos como ... se um item é um "pedido", mostra o bloco HTML do pedido. Se um item de linha puder ser copiado, imprima o bloco HTML que exibe um ícone e os parâmetros HTML por trás dele, permitindo que você faça a cópia. Se o item puder ser movido para cima ou para baixo, exiba as setas HTML apropriadas. Etc. Eu posso, através do Zend Framework, criarpartial()chama, o que significa essencialmente "chamar uma função que pega seus parâmetros e os insere em um arquivo HTML separado que também chama". Dependendo do detalhamento que desejo obter, posso criar funções HTML separadas para as menores partes do razão. Um para seta para cima, seta para baixo, outro para "posso copiar este item" etc. etc. Crie facilmente vários arquivos apenas para exibir uma pequena parte da página da web. Tomando meu código e o código do Zend Framework nos bastidores, o sistema / pilha provavelmente chama perto de 20 a 30 arquivos diferentes.

O que?

Estou interessado em aspectos, o desgaste da máquina criada pela compartimentação do código em muitos arquivos separados menores.

Por exemplo, carregar mais arquivos significa tê-los localizados em vários locais do sistema de arquivos e em vários locais do HDD físico, o que significa mais tempo de busca e leitura do HDD.

Para a CPU, provavelmente significa mais alternância de contexto e carregamento de vários registros.

Neste sub-bloco (atualização 2), estou interessado mais estritamente em como o uso de vários arquivos para executar as mesmas tarefas que poderiam ser realizadas em um único arquivo afeta o desempenho do sistema.

Usando a API do Zend Form vs HTML simples

Usei a API do Zend Form com as melhores e mais modernas práticas de OO, para criar um formulário HTML com validação, transformando-os POSTem objetos de domínio.

Levei 35 arquivos para fazer isso.

35 files = 
    = 10 fieldsets x {programmatic fieldset + fieldset manager + view template} 
    + a few supporting files

Tudo isso pode ser substituído por alguns arquivos HTML + PHP + JS + CSS simples, talvez um total de 4 arquivos leves.

É melhor? Vale a pena? ... Imagine carregar 35 arquivos + vários arquivos da biblioteca Zend Zramework que os fazem funcionar, em comparação a 4 arquivos simples.

Dennis
fonte
11
Pergunta incrível. Farei alguns testes comparativos (leve-me um dia ou mais para encontrar bons casos). No entanto, os aprimoramentos de velocidade nesse nível acarretam um custo enorme para os custos de legibilidade e desenvolvimento. Meu palpite inicial é que o resultado são ganhos insignificantes de desempenho.
Dan Sabin
7
@ Dan: Você pode colocá-lo no seu calendário para comparar o código após 1, 5 e 10 anos de manutenção. Se eu me lembro Vou voltar para verificar os resultados :)
mattnz
11
Sim, esse é o verdadeiro kicker. Acho que todos concordamos que fazer menos pesquisas e chamadas de função é mais rápido. No entanto, não consigo imaginar um caso em que isso fosse preferido a coisas como treinar de maneira fácil e fácil novos membros da equipe.
Dan Sabin
2
Para esclarecer, você tem requisitos de velocidade específicos para o seu projeto. Ou você quer apenas que seu código "vá mais rápido!" Se for o último, eu não me preocuparia, o código que é rápido o suficiente, mas fácil de manter, é muito melhor do que o código que é mais rápido do que rápido o suficiente, mas é uma bagunça.
Cormac Mulhall
4
A idéia de evitar chamadas de função por razões de desempenho é exatamente o tipo de pensamento maluco que Dijkstra criticou em sua famosa citação sobre otimização pré-madura. Sério, eu não posso
RibaldEddie 4/14/14

Respostas:

10

Minha pergunta é a seguinte: quando se trata de código de máquina, 1s e 0s, instruções de montagem, devo me preocupar com o fato de que meu código separado por classe com várias funções pequenas a pequenas gera muita sobrecarga extra?

MINHA resposta é sim, você deveria. Não porque você tenha muitas funções pequenas (uma vez a sobrecarga das funções de chamada era razoavelmente significativa e você poderia atrasar o programa fazendo um milhão de pequenas chamadas em loops, mas hoje os compiladores as alinham para você e o que resta é levado cuidado com os algoritmos de previsão sofisticados da CPU, então não se preocupe com isso), mas porque você introduzirá o conceito de camadas demais em seus programas quando a funcionalidade for muito pequena para parecer sensata. Se você possui componentes maiores, pode estar razoavelmente certo de que eles não executam o mesmo trabalho repetidamente, mas você pode tornar seu programa tão minuciosamente granulado que talvez não consiga realmente entender os caminhos da chamada e, com isso, acabar com algo isso mal funciona (e dificilmente pode ser mantido).

Por exemplo, trabalhei em um local que me mostrou um projeto de referência para um serviço da web com 1 método. O projeto compreendeu 32 arquivos .cs - para um único serviço da web! Imaginei que era muita complexidade, mesmo que cada parte fosse minúscula e fácil de entender por si só, quando se tratava de descrever o sistema geral, rapidamente me vi tendo que rastrear chamadas apenas para ver o que diabos estava fazendo (lá também havia muitas abstrações envolvidas, como seria de esperar). Meu serviço web de substituição foi de 4 arquivos .cs.

não medi o desempenho, pois acho que seria praticamente o mesmo, mas posso garantir que o meu era significativamente mais barato de manter. Quando todo mundo fala que o tempo do programador é mais importante que o tempo da CPU, crie monstros complexos que custam muito tempo no programador e na manutenção. Você deve se perguntar se eles estão dando desculpas por mau comportamento.

Foi dito em algum lugar (citação TBD) que até 70% de todo o código é constituído pela instrução MOV do ASM - carregando os registros da CPU com variáveis ​​apropriadas, não a computação real sendo feita.

Isso é o que CPUs fazer, porém, eles se movem bits da memória aos registos, adicionar ou subtrair-los, e, em seguida, colocá-los de volta na memória. Toda a computação se resume a isso. Lembre-se, eu já tive um programa muito multiencadeado que passava a maior parte do tempo alternando de contexto (ou seja, salvando e restaurando o estado do registro dos encadeamentos) do que trabalhando no código do encadeamento. Uma simples trava no lugar errado realmente atrapalhava o desempenho ali, e era um código tão inócuo também.

Portanto, meu conselho é: encontre um meio termo sensato entre os dois extremos que faça com que seu código pareça bom para outros seres humanos e teste o sistema para verificar se ele funciona bem. Use os recursos do sistema operacional para garantir que esteja funcionando como seria de esperar com CPU, memória, disco e E / S de rede.

gbjbaanb
fonte
Eu acho que isso fala mais comigo agora. Sugere-me que um bom meio termo é começar com conceitos de mapeamento mental para codificar partes enquanto segue e usa certos conceitos (como DI), em vez de ficar preso à decomposição esotérica de código e enlouquecer (ou seja, DI tudo que você precisa ou não).
Dennis
11
Pessoalmente, acho que um código mais "moderno" é um pouco mais fácil de criar um perfil ... Acho que quanto mais sustentável é um pedaço de código, também é mais fácil criar um perfil, mas há um limite em que dividir as coisas em pedaços menos
manutenível
25

Sem muito cuidado, a micro otimização, como essas preocupações, leva a um código não sustentável.

Inicialmente, parece uma boa idéia, o criador de perfil informa que o código é mais rápido e o V&V / Test / QA diz que funciona. Logo que erros são encontrados, mudanças nos requisitos e melhorias que nunca foram consideradas são solicitadas.

Durante a vida de um projeto, o código é degradado e se torna menos eficiente. O código de manutenção se tornará mais eficiente do que seu equivalente não-sustentável, pois se degradará mais lentamente. O motivo é que o código cria entropia à medida que é alterado -

Código não-sustentável rapidamente possui mais código morto, caminhos redundantes e duplicação. Isso leva a mais erros, criando um ciclo de degradação do código - incluindo seu desempenho. Em pouco tempo, os desenvolvedores têm pouca confiança de que as alterações que estão fazendo estão corretas. Isso os torna mais lentos, cautelosos e geralmente levam a ainda mais entropia, pois abordam apenas os detalhes que podem ser vistos.

O código de manutenção, com pequenos módulos e testes de unidade, é mais fácil de alterar, o código que não é mais necessário, é mais fácil de identificar e remover. O código quebrado também é mais fácil de identificar, pode ser reparado ou substituído com confiança.

Portanto, no final, tudo se resume ao gerenciamento do ciclo de vida e não é tão simples quanto "isso é mais rápido, portanto sempre será mais rápido".

Acima de tudo, o código correto lento é infinitamente mais rápido que o código incorreto rápido.

mattnz
fonte
Thankx. Para direcionar isso um pouco para onde eu estava indo, não estou falando sobre micro-otimização, mas um movimento global para escrever código menor, incorporando mais injeção de dependência e, portanto, mais partes de funcionalidade externas e mais "partes móveis" de código em geral que todos precisam estar conectados para que funcionem. Costumo pensar que isso produz mais flange de ligação / conector / passagem variável / MOV / PUSH / POP / CALL / JMP no nível do hardware. Também vejo um valor na mudança para a legibilidade do código, embora à custa de sacrificar os ciclos de computação no nível do hardware por "fluff".
Dennis
10
Evitar chamadas de função por razões de desempenho é absolutamente uma micro-otimização! A sério. Não consigo pensar em um exemplo melhor de micro-otimização. Que evidência você tem de que a diferença de desempenho realmente importa para o tipo de software que você está escrevendo? Parece que você não tem nenhum.
precisa saber é o seguinte
17

Pelo que entendi, como você indica com o inline, em formas de código de nível inferior, como C ++, isso pode fazer a diferença, mas digo CAN de ânimo leve.

O site resume tudo - não há resposta fácil . Depende do sistema, depende do que sua aplicação está fazendo, depende do idioma, depende do compilador e da otimização.

C ++, por exemplo, embutido pode aumentar o desempenho. Muitas vezes, pode não fazer nada, ou possivelmente diminuir o desempenho, mas eu pessoalmente nunca encontrei isso, apesar de já ter ouvido histórias. Inline nada mais é do que uma sugestão para o compilador otimizar, o que pode ser ignorado.

As chances são de que, se você estiver desenvolvendo programas de nível superior, a sobrecarga não deve ser uma preocupação se houver um para começar. Atualmente, os compiladores são extremamente inteligentes e devem lidar com isso de qualquer maneira. Muitos programadores têm um código a seguir: nunca confie no compilador. Se isso se aplica a você, mesmo pequenas otimizações que você considera importantes podem ser. Mas, lembre-se, todo idioma difere nesse sentido. Java faz otimizações embutidas automaticamente em tempo de execução. Em Javascript, o inline para sua página da Web (ao contrário de arquivos separados) é um impulso e cada milissegundo de uma página da Web pode contar, mas isso é mais um problema de IO.

Porém, em programas de nível inferior, nos quais o programador pode estar trabalhando muito com o código da máquina, juntamente com algo como C ++, o ouvinte pode fazer toda a diferença. Os jogos são um bom exemplo de onde o pipelining da CPU é crítico, especialmente em consoles, e algo como inline pode se somar um pouco aqui e ali.

Uma boa leitura on-line especificamente: http://www.gotw.ca/gotw/033.htm

user99999991
fonte
Vindo da perspectiva da minha pergunta, não estou tão focado em inlining per seh, mas na "fiação codificada" que ocupa o processo de tempo de CPU, barramento e E / S vinculando vários trechos de código. Gostaria de saber se existe algum momento em que há 50% ou mais do código de fiação e 50% do código real que você deseja executar. Eu imagino que haja muita penugem, mesmo no código mais rígido que se pode escrever, e isso parece ser um fato da vida. Grande parte do código real que é executado no nível de bits e bytes é logística - movendo valores de um lugar para outro, pulando para um lugar ou outro e, às vezes, adicionando apenas ...
Dennis
... subtração ou outra função relacionada aos negócios. Assim como o desenrolar do loop pode acelerar alguns loops devido à menor sobrecarga alocada para o aumento de variáveis, escrever funções maiores provavelmente pode adicionar alguma velocidade, desde que seu caso de uso esteja configurado para ele. Minha preocupação aqui é mais geral: ver muitos conselhos para escrever trechos de código menores, aumenta essa fiação, beneficiando ao acrescentar legibilidade (espero) e às custas do inchaço no nível micro.
Dennis
3
@Dennis - uma coisa a considerar é que, em uma linguagem OO, pode haver MUITO pouca correlação entre o que o programador escreve (a + b) e qual código é gerado (uma simples adição de dois registros? Move da memória primeiro? depois, a função chama o operador de um objeto +?). Portanto, 'pequenas funções' no nível do programador podem ser qualquer coisa menos pequenas, uma vez renderizadas no código da máquina.
Michael Kohne
2
@ Dennis Posso dizer que escrever código ASM (diretamente, não compilado) para Windows segue as linhas de "mov, mov, invocar, mov, mov, invocar". Com invoke, é uma macro que faz uma chamada agrupada por push / pops ... Ocasionalmente, você faz chamadas de função em seu próprio código, mas é diminuído por todas as chamadas do SO.
precisa
12

Essa é uma alteração comparada às práticas de código "antigas" ou "ruins", nas quais você tem métodos abrangendo 2500 linhas e grandes classes fazendo tudo.

Acho que ninguém nunca pensou que fazer isso é uma boa prática. E duvido que as pessoas que fizeram isso por razões de desempenho.

Eu acho que a famosa citação de Donald Knuth é muito relevante aqui:

Devemos esquecer pequenas eficiências, digamos, 97% das vezes: a otimização prematura é a raiz de todo mal.

Portanto, em 97% do seu código, use boas práticas, escreva métodos pequenos (quão pequeno é uma questão de opinião, não acho que todos os métodos devam ser apenas poucas linhas) etc. Nos 3% restantes, onde o desempenho importa, meça . E se as medidas mostrarem que ter muitos métodos pequenos realmente diminui significativamente o seu código, você deve combiná-los em métodos maiores. Mas não escreva código não sustentável apenas porque pode ser mais rápido.

svick
fonte
11

Você precisa ter cuidado para ouvir programadores experientes e também o pensamento atual. As pessoas que lidam há anos com software massivo têm algo a contribuir.

Na minha experiência, aqui está o que leva a desacelerações, e elas não são pequenas. São ordens de magnitude:

  • A suposição de que qualquer linha de código leva aproximadamente o mesmo tempo que qualquer outra. Por exemplo cout << endlversus a = b + c. O primeiro leva milhares de vezes mais que o último. O Stackexchange tem muitas perguntas no formato "Tentei maneiras diferentes de otimizar esse código, mas isso não parece fazer diferença, por que não?" quando há uma chamada de função antiga e grande no meio.

  • A suposição de que qualquer chamada de função ou método, uma vez escrita, é obviamente necessária. É fácil chamar funções e métodos, e a chamada geralmente é bastante eficiente. O problema é que eles são como cartões de crédito. Eles tentam você a gastar mais do que realmente deseja e tendem a esconder o que você gastou. Além disso, o software grande possui camadas e mais camadas de abstração, portanto, mesmo que haja apenas 15% de desperdício em cada camada, mais de 5 camadas que compõem um fator de desaceleração de 2. A resposta para isso é não remover a funcionalidade ou gravar funções maiores, é se disciplinar para ficar atento a esse problema e estar disposto e capaz de erradicá-lo .

  • Generalidade galopante. O valor da abstração é que você pode fazer mais com menos código - pelo menos essa é a esperança. Essa idéia pode ser levada ao extremo. O problema com muita generalidade é que todo problema é específico e, quando você o resolve com abstrações gerais, essas abstrações não são necessariamente capazes de explorar as propriedades específicas do seu problema. Por exemplo, eu vi uma situação em que uma classe de fila de prioridade sofisticada, que poderia ser eficiente em tamanhos grandes, era usada quando o comprimento nunca excedia 3!

  • Estrutura de dados galopante. OOP é um paradigma muito útil, mas não incentiva a minimizar a estrutura de dados - antes, incentiva a tentativa de ocultar a complexidade dela. Por exemplo, existe o conceito de "notificação" onde, se o dado A for modificado de alguma maneira, A emite um evento de notificação para que B e C também possam se modificar de modo a manter todo o conjunto consistente. Isso pode se propagar por várias camadas e aumentar enormemente o custo da modificação. Então é perfeitamente possível que a mudança para A possa logo desfeitaou alterado para mais uma modificação, o que significa que o esforço despendido na tentativa de manter o conjunto consistente deve ser feito novamente. Confuso é a probabilidade de erros em todos esses manipuladores de notificação, circularidade etc. É muito melhor tentar manter a estrutura de dados normalizada, para que qualquer alteração precise ser feita em um único local. Se dados não normalizados não puderem ser evitados, é melhor ter passes periódicos para reparar a inconsistência, em vez de fingir que eles podem ser mantidos consistentes com uma trela curta.

... quando pensar em mais, vou adicioná-lo.

Mike Dunlavey
fonte
8

A resposta curta é sim". E, geralmente, o código será um pouco mais lento.

Às vezes, porém, uma refatoração OO-ish adequada revela otimizações que tornam o código mais rápido. Eu trabalhei em um projeto no qual criamos um algoritmo Java complexo muito mais OO-ish, com estruturas de dados, getters, etc. apropriados, em vez de arrays aninhados de objetos. Porém, isolando e restringindo melhor o acesso às estruturas de dados, conseguimos mudar de matrizes gigantes de Doubles (com nulos para resultados vazios) para matrizes mais organizadas de duplas, com NaNs para resultados vazios. Isso produziu um ganho de 10x em velocidade.

Adendo: Em geral, códigos menores e melhor estruturados devem ser mais propensos a multiencadeamento, sua melhor maneira de obter grandes acelerações.

user949300
fonte
11
Eu não vejo por que mudar de Doubles de doubles requerem código melhor estruturado.
precisa saber é o seguinte
Uau, um voto negativo? O código original do cliente não lidava com o Double.NaNs, mas procurava nulos para representar valores vazios. Após a reestruturação, poderíamos lidar com isso (via encapsulamento) com os getters dos vários resultados do algoritmo. Claro, poderíamos ter reescrito o código do cliente, mas isso foi mais fácil.
user949300
Para que conste, não fui eu quem rebaixou esta resposta.
svick
7

Entre outras coisas, a programação é sobre trade-offs . Com base nesse fato, inclino-me a responder que sim, poderia ser mais lento. Mas pense sobre o que você recebe em troca. Obter código legível, reutilizável e facilmente modificável supera facilmente todas as desvantagens possíveis.

Como o @ user949300 mencionou , é mais fácil identificar áreas que podem ser aprimoradas em termos de algoritmo ou arquitetura com essa abordagem; melhorá-las é geralmente muito mais benéfico e eficaz do que não ter OO possível ou sobrecarga de chamada de função (o que já é apenas um ruído, aposto).


Também imagino que boas otimizações podem ser feitas no ASM antes que ele seja realmente executado no hardware.

Sempre que algo assim me passa pela cabeça, lembro que décadas passadas pelas pessoas mais inteligentes que trabalham em compiladores provavelmente estão tornando ferramentas como o GCC muito melhores do que eu na geração de código de máquina. A menos que você esteja trabalhando em algum tipo de material relacionado a microcontroladores, sugiro que não se preocupe com isso.

Estou assumindo que a adição de mais e mais funções e mais e mais objetos e classes em um código resultará em mais e mais parâmetros passando entre pedaços de código menores.

Supondo que qualquer coisa ao otimizar seja uma perda de tempo, você precisa de fatos sobre o desempenho do código. Descubra onde você programa passa a maior parte do tempo com ferramentas especializadas, otimize isso e repita.


Para resumir tudo; deixe o compilador fazer seu trabalho, concentre-se em coisas importantes, como melhorar seus algoritmos e estruturas de dados. Todos os padrões que você mencionou na sua pergunta existem para ajudá-lo, use-os.

PS: Essas conversas de 2 Crockford surgiram na minha cabeça e acho que elas estão um pouco relacionadas. O primeiro é uma história super rápida de CS (que é sempre bom saber com qualquer ciência exata); e o segundo é sobre por que rejeitamos coisas boas.

elmigranto
fonte
Eu realmente gosto mais desta resposta. Os seres humanos são horríveis ao adivinhar o compilador e tirar fotos onde estão os gargalos. é claro que a complexidade do tempo grande-O é algo que você deve estar ciente, mas o método espaguete grande de 100 linhas versus uma invocação de método de fábrica + algum despacho de método virtual nunca deve ser uma discussão. o desempenho não é de todo interessante nesse caso. Além disso, grande vantagem por observar que "otimizar" sem fatos e medições é uma perda de tempo.
Sara #
4

Acredito que as tendências que você identifica apontam para uma verdade sobre o desenvolvimento de software - o tempo do programador é mais caro que o tempo da CPU. Até agora, os computadores ficaram mais rápidos e mais baratos, mas uma bagunça confusa de um aplicativo pode levar centenas, senão milhares de horas-homem para mudar. Dado o custo de salários, benefícios, espaço no escritório, etc, etc, é mais econômico ter um código que pode ser executado um pouco mais devagar, mas é mais rápido e seguro mudar.

Rory Hunter
fonte
11
Concordo, mas o dispositivo móvel está se tornando tão popular que acho que é uma grande exceção. Embora o poder de processamento esteja aumentando, você não pode criar um aplicativo para iPhone e espera poder adicionar memória como em um servidor da web.
JeffO 26/02
3
@JeffO: Eu discordo - dispositivos móveis com processadores quad core agora são normais, o desempenho (especialmente porque afeta a vida da bateria), embora seja uma preocupação, é menos importante que a estabilidade. Um telefone celular ou tablet lento recebe críticas ruins e instável é abatido. Os aplicativos móveis são muito dinâmicos, mudando quase diariamente - a fonte precisa acompanhar.
mattnz
11
@ Jeff: Para o que vale a pena, a máquina virtual usada no Android é muito ruim. De acordo com os benchmarks, pode ser uma ordem de magnitude mais lenta que o código nativo (enquanto o melhor da raça geralmente é um pouco mais lento). Adivinha o que ninguém quer saber. Escrever o aplicativo é rápido e a CPU fica lá girando os polegares e aguardando a entrada do usuário 90% do tempo.
Jan Hudec
Há mais no desempenho do que os benchmarks brutos da CPU. Meu telefone Android funciona bem, exceto quando o AV está digitalizando uma atualização e, em seguida, ele parece travar por mais tempo do que eu gosto, e é um modelo de RAM de 2Gb quad-core! Hoje, a largura de banda (seja de rede ou memória) é provavelmente o principal gargalo. Sua CPU super rápida provavelmente está girando os polegares 99% do tempo e a experiência geral ainda é baixa.
Gbjbaanb
4

Bem, há mais de 20 anos, acho que você não está chamando de novo e nem "velho ou ruim", a regra era manter as funções pequenas o suficiente para caber em uma página impressa. Nós tínhamos impressoras matriciais então o número de linhas era um pouco fixo, geralmente apenas uma ou duas opções para o número de linhas por página ... definitivamente menos de 2500 linhas.

Você está perguntando a muitos lados do problema, manutenção, desempenho, testabilidade, legibilidade. Quanto mais você se inclina para o desempenho, menos fácil de manter e legível o código se tornará; portanto, você precisa encontrar seu nível de conforto que pode e variará para cada programador individual.

Quanto ao código produzido pelo compilador (código da máquina, se você preferir), quanto maior a função, maior a oportunidade de precisar derramar valores intermediários nos registros para a pilha. Quando os quadros de pilha são usados, o consumo da pilha está em pedaços maiores. Quanto menores as funções, maior a chance de os dados permanecerem mais registrados e menor a dependência da pilha. Pedaços menores de pilha são necessários por função naturalmente. Os quadros de pilha têm prós e contras de desempenho. Mais funções menores significam mais configuração e limpeza de funções. Obviamente, também depende de como você compila, quais oportunidades você oferece ao compilador. Você pode ter 250 funções de 10 linhas em vez de uma função de 2500 linhas, a única função de 2500 linhas que o compilador obterá se puder / optar por otimizar a coisa toda. Mas se você pegar essas 250 funções de 10 linhas e distribuí-las por 2, 3, 4, 250 arquivos separados, compile cada arquivo separadamente, então o compilador não será capaz de otimizar quase o máximo de código morto possível. a linha inferior aqui é que existem prós e contras para ambos e não é possível colocar uma regra geral sobre isso ou aquele é o melhor caminho.

Funções de tamanho razoável, algo que uma pessoa pode ver em uma tela ou página (em uma fonte razoável), é algo que ela pode consumir melhor e entender do que um código bastante grande. Mas se for apenas uma função pequena com chamadas para muitas outras funções pequenas que chamam muitas outras funções pequenas, você precisará de várias janelas ou navegadores para entender esse código, para que você não compre nada do lado da legibilidade.

a maneira unix é fazer usando meu termo, blocos de lego bem polidos. Por que você usaria uma função de fita tantos anos depois que paramos de usar fitas? Como o blob fez seu trabalho muito bem e, no verso, podemos substituir a interface da fita por uma interface de arquivo e tirar proveito do conteúdo do programa. Por que reescrever o software de gravação de cdrom apenas porque o scsi desapareceu como a interface dominante substituída por ide (para voltar mais tarde). Novamente, aproveite a vantagem dos sub-blocos polidos e substitua uma extremidade por um novo bloco de interface (também entenda que os projetistas de hardware simplesmente colocaram um bloco de interface nos projetos de hardware para, em alguns casos, fazer com que uma unidade scsi tenha uma interface ide-scsi. , crie blocos lego polidos de tamanho razoável, cada um com uma finalidade bem definida e entradas e saídas bem definidas. você pode agrupar testes em torno desses blocos lego e, em seguida, usar a mesma interface de usuário e as interfaces de sistema operacional em torno do mesmo bloco e do bloco; em teoria, ser bem testado e bem entendido não precisará ser depurado, apenas os novos blocos adicionais adicionados. em cada extremidade. contanto que todas as suas interfaces de blocos sejam bem projetadas e a funcionalidade seja bem compreendida, você pode criar muitas coisas com o mínimo ou qualquer cola. Assim como nos blocos de lego azul e vermelho e preto e amarelo de tamanhos e formas conhecidos, você pode fazer muitas coisas. contanto que todas as suas interfaces de blocos sejam bem projetadas e a funcionalidade seja bem compreendida, você pode criar muitas coisas com o mínimo ou qualquer cola. Assim como nos blocos de lego azul e vermelho e preto e amarelo de tamanhos e formas conhecidos, você pode fazer muitas coisas. contanto que todas as suas interfaces de blocos sejam bem projetadas e a funcionalidade seja bem compreendida, você pode criar muitas coisas com o mínimo ou qualquer cola. Assim como nos blocos de lego azul e vermelho e preto e amarelo de tamanhos e formas conhecidos, você pode fazer muitas coisas.

Cada indivíduo é diferente, sua definição de polido, bem definido, testado e legível varia. Não é razoável, por exemplo, que um professor dite regras de programação não porque elas podem ou não ser ruins para você como profissional, mas, em alguns casos, para facilitar a tarefa de ler e classificar seu código no professor ou nos assistentes de estudantes de pós-graduação. ... Você é igualmente provável, profissionalmente, a descobrir que cada trabalho pode ter regras diferentes por várias razões, geralmente uma ou poucas pessoas no poder têm sua opinião sobre algo, certo ou errado e com esse poder elas podem ditar que você faz lá do jeito (ou sair, ser demitido ou de alguma forma chegar ao poder). Essas regras são tão frequentemente baseadas em opiniões quanto em algum tipo de fato sobre legibilidade, desempenho, testabilidade, portabilidade.

old_timer
fonte
“Então você precisa encontrar o seu nível de conforto que pode e variará para cada programador individual”. Acho que o nível de otimização deve ser decidido pelos requisitos de desempenho de cada parte do código, não pelo que cada programador gosta.
svick
O compilador com certeza, assumindo que o programador disse ao compilador para otimizar os compiladores geralmente padrão para não otimizar (na linha de comando, o IDE pode ter um padrão diferente se usado). Mas o programador pode não saber o suficiente para otimizar o tamanho da função e com tantos alvos onde isso fica inútil? O desempenho do ajuste manual tem uma tendência a afetar negativamente a legibilidade e, em particular, a capacidade de manutenção, a testabilidade pode ser feita de qualquer maneira.
58568 Old_timer
2

Depende da inteligência do seu compilador. Geralmente, tentar enganar o otimizador é uma péssima idéia e pode realmente roubar ao compilador as oportunidades de otimização. Para iniciantes, você provavelmente não tem idéia do que ele pode fazer e a maior parte do que você faz realmente influencia o quão bem ele faz isso.

Otimização prematura é a noção de programadores que tentam fazer isso e terminam com códigos difíceis de manter que não estavam realmente no caminho crítico do que estavam tentando fazer. Tentar extrair o máximo de CPU possível quando na maioria das vezes seu aplicativo está realmente bloqueado aguardando eventos de E / S, é algo que vejo muito, por exemplo.

O melhor é codificar a correção e usar um criador de perfil para encontrar gargalos reais de desempenho e corrigi-los analisando o que está no caminho crítico e se ele pode ser aprimorado. Muitas vezes, algumas correções simples são úteis.

Jilles van Gurp
fonte
0

[elemento do tempo do computador]

A refatoração para acoplamento mais flexível e funções menores afeta a velocidade do código?

Sim. Mas cabe aos intérpretes, compiladores e compiladores JIT reduzir esse código de "costura / fiação", e alguns o fazem melhor que outros, mas outros não.

A preocupação com vários arquivos aumenta a sobrecarga de E / S, de modo que afeta a velocidade também (em tempo de computador).

[elemento da velocidade humana]

(e devo me importar?)

não, você não deveria se importar. Hoje em dia, os computadores e os circuitos são bastante rápidos e outros fatores assumem o controle, como latência da rede, E / S do banco de dados e cache.

Portanto, a desaceleração de 2x a 4x na própria execução do código nativo geralmente é abafada por esses outros fatores.

No que diz respeito ao carregamento de vários arquivos, isso geralmente está sendo tratado por várias soluções de cache. Pode levar mais tempo para carregar as coisas e mesclá-las na primeira vez, mas para a próxima vez, para arquivos estáticos, o cache funciona como se um único arquivo estivesse sendo carregado. O armazenamento em cache é uma solução para o carregamento de vários arquivos.

Dennis
fonte
0

A RESPOSTA (caso você tenha perdido)

Sim, você deve se importar, mas se preocupe com a forma como você escreve o código, e não com o desempenho.

Em resumo

Não me importo com desempenho

No contexto da pergunta, compiladores e intérpretes mais inteligentes já cuidam disso

Preocupe-se em escrever código sustentável

Código em que os custos de manutenção estão no nível da compreensão humana razoável. ou seja, não escreva 1000 funções menores, tornando o código incompreensível, mesmo que você entenda cada uma delas, e não escreva uma função de objeto divino que seja muito grande para entender, mas escreva 10 funções bem projetadas que façam sentido para o ser humano e sejam de fácil manutenção.

Dennis
fonte