ti-enxame.com

Quais técnicas simples você usa para melhorar o desempenho?

Estou falando da 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 um 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 método length apenas uma vez ... minha namorada diz que é enigmático. Existe algum outro truque simples que você usa em seus próprios desenvolvimentos?

21
Cristian

insira uma palestra prematura de discussão é a raiz de todo o 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:

for (i = 0; i < strlen(str); i++) {
    ...
}

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 GCC realmente otimiza esse caso porque strlen() é marcado como uma função pura.

Ao classificar um milhão de inteiros de 32 bits, a classificação de bolhas 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 não tiver um índice de pessoas (idade), será necessária uma verificação seqüencial O(n) em vez de um O muito mais rápido log n) varredura de índice.

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 geralmente otimiza coisas como n / 2 Para n >> 1 Automaticamente se você estiver direcionando para um computador convencional, mas se estiver direcionando para um dispositivo incorporado, talvez não tenha esse luxo.

Além disso, % 2 E & 1 Têm semânticas diferentes. A divisão e o módulo geralmente arredondam para zero, mas sua implementação é definida. O bom e velho >> E & Sempre se aproximam do infinito negativo, o que (na minha opinião) faz muito mais sentido. Por exemplo, no meu computador:

printf("%d\n", -1 % 2); // -1 (maybe)
printf("%d\n", -1 & 1); // 1

Portanto, use o que faz sentido. Não pense que você está sendo um bom garoto usando % 2 Quando originalmente escreveria & 1.

Operações caras de ponto flutuante

Evite operações pesadas de ponto flutuante como pow() e log() 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:

int parseInt(const char *str)
{
    const char *p;
    int         digits;
    int         number;
    int         position;

    // Count the number of digits
    for (p = str; isdigit(*p); p++)
        {}
    digits = p - str;

    // Sum the digits, multiplying them by their respective power of 10.
    number = 0;
    position = digits - 1;
    for (p = str; isdigit(*p); p++, position--)
        number += (*p - '0') * pow(10, position);

    return number;
}

Não é apenas esse uso de pow() (e as conversões int <-> double necessárias para usá-lo) bastante caro, mas também cria uma oportunidade para perda de precisão (aliás , o código acima não tem 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:

int parseInt(const char *str)
{
    const char *p;
    int         number;

    number = 0;
    for (p = str; isdigit(*p); p++) {
        number *= 10;
        number += *p - '0';
    }

    return number;
}
28
Joey Adams

A 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 Kent Beck :

"Faça funcionar, faça certo, faça rápido".

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 esse for o meu ' escolhido não está funcionando conforme o esperado.

Por exemplo, para testar a velocidade com o código .NET, use atributo Timeout do NUnit para escrever afirmações de que uma chamada para um método específico será executada dentro de um determinado 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 o 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.

13
Paddyslacker

Lembre-se de que seu compilador pode virar:

for(int i = 0; i < collection.length(); i++ ){
   // stuff here
}

para dentro:

int j = collection.length();
for(int i = 0; i < j; i++ ){
   // stuff here
}

ou algo semelhante, se collection for inalterado no 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ê alterar collection dentro do loop adicionando ou removendo elementos (sim, eu sei que é uma péssima idéia, mas acontece), então o seu segundo exemplo não repetirá todos os elementos ou tentará acessar após o final da matriz.

11
ChrisF

Esse tipo de otimização geralmente não é recomendado. Essa parte da otimização pode ser facilmente feita pelo compilador; você está trabalhando com uma linguagem de programação de nível superior em vez do Assembly, então pense no mesmo nível.

9
tactoth

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.

3
Guffa

Isso pode não se aplicar muito à codificação de uso geral, mas atualmente faço principalmente desenvolvimento incorporado. 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:

 bool isReady = (value > TriggerLevel);

e o transforma no conjunto equivalente a

isReady = 0
if (value > TriggerLevel)
{
  isReady = 1;
}

Isso levará 3 ciclos ou 10 se saltar sobre isReady=1;. Mas o processador possui uma instrução max de ciclo único, por isso é muito melhor escrever código para gerar essa sequência, que é garantida para sempre levar 3 ciclos:

diff = value-TriggerLevel;
diff = max(diff, 0);
isReady = min(1,diff);

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:

#define BOOL_GT(a,b) min(max((a)-(b),0),1)

//isReady = value > TriggerLevel;
isReady = BOOL_GT(value, TriggerLevel);

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.

3
AShelly

Eu tenho uma técnica muito simples.

  1. Eu faço meu código funcionar.
  2. Eu testei a velocidade.
  3. Se for rápido, volto à etapa 1 para algum outro recurso. Se for lento, eu perfil para encontrar o gargalo.
  4. Eu conserto o gargalo. Volte para a etapa 1.

Há muitas vezes que 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.

3
Jason Baker

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 ao longo do tempo.

2
realworldcoder
  1. Perfil. Nós temos algum problema? Onde?
  2. Em 90% dos casos, de alguma forma IO relacionado, aplique o cache (e talvez obtenha mais memória)
  3. Se estiver relacionado à CPU, aplique o cache
  4. Se o desempenho ainda é um problema, deixamos o domínio das técnicas simples - faça as contas.
1
Maglob

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

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    int* values = calloc(n, sizeof(int));

    // do stuff with values
    ...
    free(values);
}

E depois:

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    int values_mem[64] = {0}
    int* values = (n <= 64) ? values_mem: calloc(n, sizeof(int));

    // do stuff with values
    ...
    if (values != values_mem)
        free(values);
}

Eu generalizei isso assim, pois esses tipos de pontos de acesso aparecem muito nos perfis:

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    MemFast values_mem;
    int* values = mf_calloc(&values_mem, n, sizeof(int));

    // do stuff with values
    ...

    mf_free(&values_mem);
}

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 caso contrário.

No C++, generalizei isso com uma pequena sequência compatível com o padrão (semelhante às implementações SmallVector 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.

1
user204677

se 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, além de você não ser tão inteligente quanto o compilador de otimização.

1
Job

Espere seis meses, peça ao seu chefe para comprar computadores novos para todos. Seriamente. 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.

1
Michael Kristofik

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:

for(int i = 0, j = collection.length(); i < j; i++ ){
// stuff here
}

pode se tornar

for(int i = collection.length(); i > 0; i-=1 ){
// stuff here
}

Em muitos idiomas, desde que você faça os ajustes apropriados na parte "material" e ela ainda pode ser lida. Simplesmente não aborda o problema da maneira que a maioria das pessoas pensaria em fazê-lo primeiro, porque conta de trás para frente.

em c # por exemplo:

        string[] collection = {"a","b"};

        string result = "";

        for (int i = 0, j = collection.Count() - 1; i < j; i++)
        {
            result += collection[i] + "~";
        }

também pode ser escrito como:

        for (int i = collection.Count() - 1; i > 0; i -= 1)
        {
            result = collection[i] + "~" + result;
        }

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

1
Bill

Eu tenho uma opinião um pouco diferente. 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 do desempenho (criação de perfil) e mostrar como, de muitas maneiras, o que está custando muitos ciclos é as muitas camadas de abstração, com propriedades e notificações que se propagam por todo o estrutura tentando mantê-lo 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.

0
Mike Dunlavey

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.

0
HLGEM

Se for C++, você deve adquirir o hábito de ++i ao invés de i++. ++i nunca será pior, significa exatamente o mesmo que uma declaração independente 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.

0
David Thornley