Estou falando sobre a maneira como escrevemos rotinas simples para melhorar o desempenho sem dificultar a leitura do código ... por exemplo, este é o típico que aprendemos:
for(int i = 0; i < collection.length(); i++ ){
// stuff here
}
Mas eu costumo fazer isso quando a foreach
não é aplicável:
for(int i = 0, j = collection.length(); i < j; i++ ){
// stuff here
}
Eu acho que essa é uma abordagem melhor, já que chamará o length
método apenas uma vez ... minha namorada diz que é enigmático. Existe algum outro truque simples que você usa em seus próprios desenvolvimentos?
code-quality
performance
Cristian
fonte
fonte
Respostas:
inserir palestra prematura-discussão-é-a-raiz-de-todo-mal
Dito isso, aqui estão alguns hábitos que eu entrei para evitar eficiência desnecessária e, em alguns casos, tornem meu código mais simples e mais correto também.
Esta não é uma discussão de princípios gerais, mas de algumas coisas a serem observadas para evitar a introdução de ineficiências desnecessárias no código.
Conheça o seu big-O
Provavelmente, isso deve ser mesclado na longa discussão acima. É praticamente senso comum que um loop dentro de um loop, onde o loop interno repita um cálculo, seja mais lento. Por exemplo:
Isso levará uma quantidade enorme de tempo se a string for realmente longa, porque o comprimento está sendo recalculado a cada iteração do loop. Observe que o GCC realmente otimiza esse caso porque
strlen()
está marcado como uma função pura.Ao classificar um milhão de números inteiros de 32 bits, a classificação por bolha seria o caminho errado . Em geral, a classificação pode ser feita no tempo O (n * log n) (ou melhor, no caso da classificação radix); portanto, a menos que você saiba que seus dados serão pequenos, procure um algoritmo que seja pelo menos O (n * log n).
Da mesma forma, ao lidar com bancos de dados, esteja ciente dos índices. Se você
SELECT * FROM people WHERE age = 20
e você não tiver um índice de pessoas (idade), será necessária uma varredura sequencial O (n) em vez de uma varredura muito mais rápida do índice O (log n).Hierarquia aritmética inteira
Ao programar em C, lembre-se de que algumas operações aritméticas são mais caras que outras. Para números inteiros, a hierarquia é mais ou menos assim (menos cara primeiro):
+ - ~ & | ^
<< >>
*
/
Concedido, o compilador coisas normalmente otimizar como
n / 2
paran >> 1
automaticamente se você está alvejando um computador mainstream, mas se você está alvejando um dispositivo embutido, você não pode ter esse luxo.Além disso,
% 2
e& 1
tem semântica diferente. A divisão e o módulo geralmente arredondam para zero, mas sua implementação é definida. O bom e velho>>
e&
sempre rodadas em direção ao infinito negativo, o que (na minha opinião) faz muito mais sentido. Por exemplo, no meu computador:Portanto, use o que faz sentido. Não pense que você está sendo um bom garoto usando
% 2
quando estava originalmente escrevendo& 1
.Operações caras de ponto flutuante
Evite operações pesadas de ponto flutuante como
pow()
elog()
no código que realmente não precisa delas, especialmente ao lidar com números inteiros. Veja, por exemplo, a leitura de um número:Não é apenas esse uso
pow()
(e asint
<->double
conversões necessárias para usá-lo) bastante caro, como também cria uma oportunidade para perda de precisão (aliás, o código acima não possui problemas de precisão). É por isso que estremeço quando vejo esse tipo de função usada em um contexto não matemático.Além disso, observe como o algoritmo "inteligente" abaixo, que se multiplica por 10 em cada iteração, é realmente mais conciso do que o código acima:
fonte
strlen()
examina a string apontada pelo argumento do ponteiro, o que significa que não pode ser const. Além disso,strlen()
é realmente marcado como puro no glibcstring.h
pure
ouconst
e até o documentado no arquivo de cabeçalho por causa da diferença sutil entre os dois. docs.parrot.org/parrot/1.3.0/html/docs/dev/c_functions.pod.htmlA partir da sua pergunta e do tópico do comentário, parece que você "pensa" que essa alteração no código melhora o desempenho, mas você realmente não sabe se faz ou não.
Sou fã da filosofia de Kent Beck :
Minha técnica para melhorar o desempenho do código é primeiro obter o código que passa nos testes de unidade e bem fatorado e, em seguida (especialmente para operações de loop), escrever um teste de unidade que verifique o desempenho e depois refatorar o código ou pensar em um algoritmo diferente, se o que eu for ' escolhido não está funcionando conforme o esperado.
Por exemplo, para testar a velocidade com o código .NET, uso o atributo Timeout do NUnit para escrever afirmações de que uma chamada para um método específico será executada dentro de um certo período de tempo.
Usando algo como o atributo timeout do NUnit com o exemplo de código que você deu (e um grande número de iterações para o loop), você pode realmente provar se a sua "melhoria" no código realmente ajudou ou não no desempenho desse loop.
Um aviso: Embora isso seja eficaz no nível "micro", certamente não é a única maneira de testar o desempenho e não leva em conta os problemas que possam surgir no nível "macro" - mas é um bom começo.
fonte
Lembre-se de que seu compilador pode virar:
para dentro:
ou algo semelhante, se
collection
não for alterado ao longo do loop.Se esse código estiver em uma seção de tempo crítico do seu aplicativo, vale a pena descobrir se é esse o caso ou não - ou mesmo se você pode alterar as opções do compilador para fazer isso.
Isso manterá a legibilidade do código (como o primeiro é o que a maioria das pessoas espera ver), enquanto ganha alguns desses ciclos extras de máquina. Você fica livre para se concentrar nas outras áreas em que o compilador não pode ajudá-lo.
Em uma nota lateral: se você mudar
collection
dentro do loop adicionando ou removendo elementos (sim, eu sei que é uma má ideia, mas acontece), então o seu segundo exemplo não repetirá todos os elementos ou tentará acessar o passado o fim da matriz.fonte
Esse tipo de otimização geralmente não é recomendado. Essa parte da otimização pode ser feita facilmente pelo compilador; você está trabalhando com uma linguagem de programação de nível superior em vez de assembly, portanto, pense no mesmo nível.
fonte
Isso pode não se aplicar muito à codificação de uso geral, mas atualmente faço o desenvolvimento incorporado principalmente. Temos um processador de destino específico (que não ficará mais rápido - ele parecerá obsoleto no momento em que se aposentar o sistema em mais de 20 anos) e prazos de tempo muito restritivos para grande parte do código. O processador, como todos os processadores, possui algumas peculiaridades em relação a quais operações são rápidas ou lentas.
Temos uma técnica usada para garantir que estamos gerando o código mais eficiente, mantendo a legibilidade para toda a equipe. Nos locais em que a construção da linguagem mais natural não gera o código mais eficiente, criamos macros que garantem que o código ideal seja usado. Se fizermos um projeto subseqüente para um processador diferente, poderemos atualizar as macros para o método ideal nesse processador.
Como um exemplo específico, para o nosso processador atual, as ramificações esvaziam o pipeline, paralisando o processador por 8 ciclos. O compilador aceita este código:
e o transforma no conjunto equivalente a
Isso levará 3 ciclos ou 10 se ele pular
isReady=1;
. Mas o processador possui umamax
instrução de ciclo único , por isso é muito melhor escrever código para gerar essa sequência, que é garantida para sempre levar 3 ciclos:Obviamente, a intenção aqui é menos clara que a original. Por isso, criamos uma macro, que usamos sempre que queremos uma comparação booleana Maior que:
Podemos fazer coisas semelhantes para outras comparações. Para quem está de fora, o código é um pouco menos legível do que se usássemos apenas a construção natural. No entanto, rapidamente fica claro depois de passar um pouco de tempo trabalhando com o código e é muito melhor do que permitir que todo programador experimente suas próprias técnicas de otimização.
fonte
Bem, o primeiro conselho seria evitar essas otimizações prematuras até que você saiba exatamente o que está acontecendo com o código, para ter certeza de que está realmente tornando-o mais rápido e não mais lento.
Em C #, por exemplo, o compilador otimizará o código se você estiver repetindo o comprimento de uma matriz, pois sabe que não precisa verificar o índice durante o intervalo ao acessar a matriz. Se você tentar otimizá-lo colocando o comprimento da matriz em uma variável, interromperá a conexão entre o loop e a matriz e tornará o código muito mais lento.
Se você pretende otimizar, deve limitar-se a coisas conhecidas por usar muitos recursos. Se houver apenas um pequeno ganho de desempenho, você deve usar o código mais legível e de manutenção. Como o trabalho do computador muda com o tempo, algo que você descobre ser um pouco mais rápido agora, pode não continuar assim.
fonte
Eu tenho uma técnica muito simples.
Muitas vezes, economiza tempo para contornar esse processo, mas em geral você saberá se é esse o caso. Se houver dúvida, eu continuo com isso por padrão.
fonte
Aproveite o curto-circuito:
if(someVar || SomeMethod())
leva tanto tempo para codificar e é tão legível quanto:
if(someMethod() || someVar)
no entanto, avaliará mais rapidamente com o tempo.
fonte
Espere seis meses, peça ao seu chefe para comprar computadores novos para todos. A sério. O tempo do programador é muito mais caro que o hardware a longo prazo. Os computadores de alto desempenho permitem que os codificadores escrevam o código de maneira direta, sem se preocupar tanto com a velocidade.
fonte
Tente não otimizar muito antes do tempo, quando otimizar se preocupe um pouco menos com a legibilidade.
Há pouco que odeio mais do que complexidade desnecessária, mas quando você atinge uma situação complexa, muitas vezes é necessária uma solução complexa.
Se você escrever o código da maneira mais óbvia, faça um comentário explicando por que ele foi alterado quando você faz uma alteração complexa.
Especificamente para o seu significado, acho que muitas vezes fazer o oposto booleano da abordagem padrão às vezes ajuda:
pode se tornar
Em muitos idiomas, desde que você faça os ajustes apropriados na parte "material" e ela ainda pode ser lida. Ele simplesmente não aborda o problema da maneira que a maioria das pessoas pensaria em fazê-lo primeiro, porque conta para trás.
em c # por exemplo:
também pode ser escrito como:
(e sim, você deve fazer isso com uma junção ou um construtor de strings, mas estou tentando fazer um exemplo simples)
Existem muitos outros truques que se pode usar que não são difíceis de seguir, mas muitos deles não se aplicam a todos os idiomas, como usar o meio no lado esquerdo de uma tarefa no antigo vb para evitar a penalidade de redesignação de caracteres ou ler arquivos de texto no modo binário no .net para ultrapassar a penalidade de buffer quando o arquivo é muito grande para uma leitura.
O único outro caso realmente genérico que eu possa pensar que se aplicaria a todos os lugares seria aplicar alguma álgebra booleana a condicionais complexos para tentar transformar a equação em algo que tivesse mais chances de tirar proveito de um condicional em curto-circuito ou transformar um complexo conjunto de instruções if-then ou case aninhadas inteiramente em uma equação. Nenhum deles funciona em todos os casos, mas podem economizar muito tempo.
fonte
fonte
Use as melhores ferramentas que você pode encontrar - bom compilador, bom profiler, boas bibliotecas. Acerte os algoritmos, ou melhor ainda - use a biblioteca certa para fazer isso por você. As otimizações triviais do loop são pequenas batatas, além de você não ser tão inteligente quanto o compilador de otimização.
fonte
O mais simples para mim é usar a pilha quando possível sempre que um padrão de uso de caso comum se encaixa em um intervalo de, digamos, [0, 64), mas tem casos raros que não têm um limite superior pequeno.
Exemplo simples de C (antes):
E depois:
Eu generalizei isso assim, pois esses tipos de pontos de acesso aparecem muito nos perfis:
O exemplo acima usa a pilha quando os dados que estão sendo alocados são pequenos o suficiente nesses casos de 99,9% e usa o heap de outra maneira.
Em C ++, generalizei isso com uma pequena sequência compatível com o padrão (semelhante às
SmallVector
implementações existentes), que gira em torno do mesmo conceito.Não é uma otimização épica (obtive reduções de, digamos, 3 segundos para uma operação concluir até 1,8 segundos), mas requer um esforço tão trivial para aplicar. Quando você pode reduzir algo de 3 segundos para 1,8 segundos apenas introduzindo uma linha de código e alterando dois, é um bom retorno para um investimento tão pequeno.
fonte
Bem, existem muitas alterações de desempenho que você pode fazer ao acessar dados que terão um grande impacto em seu aplicativo. Se você escrever consultas ou usar um ORM para acessar um banco de dados, precisará ler alguns livros de ajuste de desempenho para o back-end do banco de dados usado. Provavelmente, você está usando técnicas conhecidas com desempenho insatisfatório. Não há razão para fazer isso, exceto a ignorância. Isso não é otimização prematura (eu amaldiçoo o cara que disse isso porque foi tão amplamente interceptado que nunca se preocupa com o desempenho); esse é um bom design.
Apenas uma amostra rápida de aprimoradores de desempenho para o SQL Server: use índices apropriados, evite cursores - use lógica baseada em conjunto, use cláusulas sargable where, não empilhe visualizações em cima de visualizações, não retorne mais dados do que você precisa ou mais colunas do que você precisa, não use subconsultas correlacionadas.
fonte
Se for C ++, você deve adquirir o hábito de
++i
nãoi++
.++i
nunca será pior, significa exatamente o mesmo que uma declaração autônoma e, em alguns casos, pode ser uma melhoria de desempenho.Não vale a pena alterar o código existente com a menor chance de ajudar, mas é um bom hábito entrar.
fonte
Eu tenho uma opinião um pouco diferente sobre isso. Simplesmente seguir o conselho que você chegar aqui não fará muita diferença, porque há alguns erros que você precisa cometer, os quais você precisa corrigir e os quais precisa aprender.
O erro que você precisa cometer é projetar sua estrutura de dados da maneira que todo mundo faz. Ou seja, com dados redundantes e muitas camadas de abstração, com propriedades e notificações que se propagam por toda a estrutura, tentando mantê-la consistente.
Então você precisa fazer o ajuste de desempenho (criação de perfil) e mostrar como, de várias maneiras, o que está custando muitos ciclos é as muitas camadas de abstração, com propriedades e notificações que se propagam por toda a estrutura, tentando mantê-la consistente.
Você pode corrigir esses problemas um pouco, sem grandes alterações no código.
Então, se você tiver sorte, poderá aprender que menos estrutura de dados é melhor e que é melhor poder tolerar inconsistências temporárias do que tentar manter muitas coisas em concordância com as ondas de mensagens.
Como você escreve loops não tem nada a ver com isso.
fonte