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


21

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


34
+1 apenas por ter uma namorada que avisará quando seu código não estiver claro.
Kristo 26/09

76
Você está apenas postando isso para nos dizer que você tem uma namorada.
Josh K

11
@ Christian: Não se esqueça de que existem otimizações do compilador que podem fazer isso por você; portanto, você pode afetar a legibilidade e não afetar o desempenho; a otimização prematura é a raiz de todo mal ... Tente evitar mais de uma declaração ou atribuição na mesma linha, não faça as pessoas lerem duas vezes ... Você deve usar da maneira normal (seu primeiro exemplo) ou colocar o segunda declaração fora do loop for (embora isso também diminua a legibilidade, pois você precisaria ler novamente para ver o que o j significa).
Tamara Wijsman

5
@ TomWij: A citação correta (e completa): "Devemos esquecer pequenas eficiências, digamos cerca de 97% das vezes: a otimização prematura é a raiz de todo mal. No entanto, não devemos desperdiçar nossas oportunidades nesses 3% críticos. "
Robert Harvey

3
@ tomwij: Se você está gastando os três por cento, então, por definição, deve fazê-lo em código crítico de tempo e não desperdiçar seu tempo com os outros 97%.
Robert Harvey

Respostas:


28

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:

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 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 = 20e 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 / 2para n >> 1automaticamente 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, % 2e & 1tem 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:

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 % 2quando estava originalmente escrevendo & 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 pow()(e as int<-> doubleconversõ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:

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;
}

Resposta muito completa.
Paddyslacker 26/09/10

1
Observe que a discussão sobre otimização prematura não se aplica ao código de lixo. Você sempre deve usar uma implementação que funcione bem em primeiro lugar.

Observe que o GCC realmente otimiza esse caso porque strlen () é marcado como uma função pura. Eu acho que você quer dizer que é uma função const, não pura.
Andy Lester

@ Andy Lester: Na verdade, eu quis dizer puro. Na documentação do GCC , ele afirma que const é um pouco mais rigoroso que puro, pois uma função const não pode ler a memória global. 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
Joey Adams

Você está certo, meu erro, e eu deveria ter verificado duas vezes. Eu tenho trabalhado no projeto Parrot anotando funções como pureou conste 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.html
Andy Lester

13

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


2
Embora eu acredite muito em perfis, também acredito que é inteligente manter em mente os tipos de dicas que Cristian está procurando. Eu sempre escolherei o mais rápido dos dois métodos igualmente legíveis. Ser forçado a otimizar pós-amadurecido não é divertido.
AShelly 26/09/10

Não há necessariamente necessidade de testes de unidade, mas sempre vale a pena gastar esses 20 minutos para verificar se algum mito de desempenho é verdadeiro ou não, especialmente porque a resposta geralmente depende do compilador e do estado do sinalizador -O e -g (ou Debug / Liberar em caso de VS).
MBq

+1 Esta resposta complementa meu comentário relacionado à própria pergunta.
Tamara Wijsman

1
@ AShelly: se estamos falando de reformulações simples da sintaxe do loop, é muito fácil fazer alterações depois do fato. Além disso, o que você acha igualmente legível pode não ser o mesmo para outros programadores. É melhor usar a sintaxe "padrão" o máximo possível e variar apenas quando for necessário.
Joeri Sebrechts 26/09/10

@ AShelly certamente, se você consegue pensar em dois métodos igualmente legíveis e escolher o menos eficiente, não está fazendo seu trabalho? Alguém realmente faria isso?
glenatron

11

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 collectionnã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 collectiondentro 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.


1
Por que não fazê-lo explicitamente?

3
Em alguns idiomas que limitam a verificação, você LENTA o código se o fizer explicitamente. Com um loop para collection.length, o compilador o move para fora e omite a verificação de limites. Com um loop para alguma constante de outro lugar do seu aplicativo, você terá uma verificação de limites em todas as iterações. É por isso que é importante medir - a intuição sobre desempenho quase nunca está certa.
Kate Gregory

1
Por isso eu disse "valeria a pena descobrir".
ChrisF

Como o compilador C # sabe que collection.length () não modifica a coleção, da mesma forma que stack.pop ()? Eu acho que seria melhor verificar a IL em vez de assumir que o compilador otimiza isso. No C ++, você pode marcar um método como const ('não altera o objeto'), para que o compilador possa fazer essa otimização com segurança.
precisa saber é o seguinte

1
Os otimizadores do @JBRW que fazem isso também estão cientes dos métodos ok-vamos-chamar-de-constância-mesmo-que-não-seja-C ++ das coleções. Afinal, você só pode verificar os limites se percebe que algo é uma coleção e sabe como obter esse tamanho.
Kate

9

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.


1
Dê-lhe um livro sobre programação;)
Joeri Sebrechts

1
+1, pois a maioria de nossas amigas provavelmente está mais interessada em Lady Gaga do que em clareza de código.
haploid

Você poderia explicar por que não é recomendado?
Macneil

@ macneil bem ... esse truque faz com que os códigos não sejam tão comuns e totalmente não funcionem, essa parte da otimização deve ser feita pelo compilador.
Tactoth 31/10/10

@ macneil Se você estiver trabalhando em um idioma de nível superior, pense no mesmo nível.
Tactoth 31/10/10

3

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:

 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 ele pular isReady=1;. Mas o processador possui uma maxinstrução 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

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

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 ao passo 1 para algum outro recurso. Se for lento, eu perfil para encontrar o gargalo.
  4. Eu conserto o gargalo. Volte para a etapa 1.

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.


2

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.


1

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.


6
Er ... E o desempenho que seus clientes veem? Você é rico o suficiente para comprar novos computadores para eles também?
Robert Harvey

2
E quase atingimos a parede do desempenho; a computação multicore é a única saída, mas a espera não fará com que seus programas a usem.
mbq 26/09/10

+1 Esta resposta complementa meu comentário relacionado à própria pergunta.
Tamara Wijsman

3
Nenhum tempo de programação é mais caro que o hardware quando você tem milhares ou milhões de usuários. O tempo do programador NÃO é mais importante do que o tempo do usuário, faça isso o mais rápido possível.
HLGEM

1
Adquira bons hábitos, pois não leva tempo para o programador, pois é o que você faz o tempo todo.
Dominique McDonnell

1

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

        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.


é uma solução, mas o compilador provavelmente emitirá avisos, pois para a maioria das classes comuns, length () retorna um tipo não assinado
stijn

Mas, ao reverter o índice, a própria iteração pode se tornar mais complexa.
Tamara Wijsman

@stijn Eu estava pensando em c # quando escrevi, mas talvez essa sugestão também se enquadre na categoria específica de idioma por esse motivo - veja a edição ... @ToWij certamente, não acho que haja muitas, se houver, sugestões dessa natureza que não correm o risco disso. Se o seu material // era algum tipo de manipulação de pilha, talvez nem seja possível reverter a lógica corretamente, mas em muitos casos isso é e não é muito confuso se feito com cuidado na maioria desses casos IMHO.
Bill

você está certo; em C ++ eu ainda preferiria o loop 'normal', mas com a chamada length () removida da iteração (como em const size_t len ​​= collection.length (); for (size_t i = 0; i <len; ++ i) {}) por duas razões: considero o loop de contagem direta 'normal' mais legível / compreensível (mas isso provavelmente é apenas porque é mais comum) e leva a chamada invariável do comprimento () do loop.
stijn

1
  1. Perfil. Nós temos algum problema? Onde?
  2. Em 90% dos casos, de alguma forma relacionada à IO, 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

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.


1

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 de outra maneira.

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


0

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

Se for C ++, você deve adquirir o hábito de ++inão i++. ++inunca 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.


0

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.

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.