Estratégias de otimização de desempenho de último recurso [fechado]


609

Já existem muitas questões de desempenho neste site, mas me ocorre que quase todas são muito específicas de problemas e bastante restritas. E quase todos repetem os conselhos para evitar a otimização prematura.

Vamos assumir:

  • o código já está funcionando corretamente
  • os algoritmos escolhidos já são ideais para as circunstâncias do problema
  • o código foi medido e as rotinas ofensivas foram isoladas
  • todas as tentativas de otimizar também serão medidas para garantir que não piorem a situação

O que eu estou procurando aqui são estratégias e truques para espremer até os últimos por cento em um algoritmo crítico quando não há mais nada a fazer além do que for preciso.

Idealmente, tente tornar o idioma das respostas agnóstico e indique as desvantagens das estratégias sugeridas, quando aplicável.

Vou adicionar uma resposta com minhas próprias sugestões iniciais e aguardamos o que mais a comunidade do Stack Overflow puder imaginar.

Respostas:


427

OK, você está definindo o problema para onde parece que não há muito espaço para melhorias. Isso é bastante raro, na minha experiência. Tentei explicar isso em um artigo do Dr. Dobbs em novembro de 1993, partindo de um programa não trivial convencionalmente bem projetado, sem desperdício óbvio, e conduzindo-o através de uma série de otimizações até que o tempo do relógio de parede fosse reduzido de 48 segundos para 1,1 segundos, e o tamanho do código-fonte foi reduzido em um fator de 4. Minha ferramenta de diagnóstico foi essa . A sequência das mudanças foi a seguinte:

  • O primeiro problema encontrado foi o uso de clusters de lista (agora chamados de "iteradores" e "classes de contêineres"), responsáveis ​​por mais da metade do tempo. Eles foram substituídos por um código bastante simples, reduzindo o tempo para 20 segundos.

  • Agora, o maior tomador de tempo é mais construção de lista. Como porcentagem, não era tão grande antes, mas agora é porque o problema maior foi removido. Eu encontro uma maneira de acelerar, e o tempo cai para 17 segundos.

  • Agora é mais difícil encontrar culpados óbvios, mas existem alguns menores sobre os quais posso fazer algo, e o tempo cai para 13 segundos.

Agora eu pareço ter atingido uma parede. As amostras estão me dizendo exatamente o que está fazendo, mas não consigo encontrar nada que possa melhorar. Depois, reflito sobre o design básico do programa, sobre sua estrutura orientada a transações e pergunto se toda a pesquisa de lista que ele está fazendo é realmente exigida pelos requisitos do problema.

Então, refiz um novo design, onde o código do programa é realmente gerado (por meio de macros do pré-processador) a partir de um conjunto menor de fontes, e no qual o programa não está constantemente descobrindo coisas que o programador sabe que são bastante previsíveis. Em outras palavras, não "interprete" a sequência de coisas a fazer, "compile".

  • Essa reformulação é feita, diminuindo o código-fonte por um fator de 4 e o tempo é reduzido para 10 segundos.

Agora, porque está ficando tão rápido, é difícil fazer uma amostra, por isso dou 10 vezes mais trabalho a fazer, mas os seguintes são baseados na carga de trabalho original.

  • Mais diagnóstico revela que está gastando tempo no gerenciamento de filas. O alinhamento destes reduz o tempo para 7 segundos.

  • Agora, um grande tomador de tempo é a impressão de diagnóstico que eu estava fazendo. Lave isso - 4 segundos.

  • Agora, os maiores tomadores de tempo são chamadas para malloc e grátis . Reciclar objetos - 2,6 segundos.

  • Continuando a amostra, ainda encontro operações que não são estritamente necessárias - 1,1 segundos.

Fator de aceleração total: 43,6

Agora, não existem dois programas iguais, mas em softwares que não são de brinquedos, sempre vi uma progressão como essa. Primeiro você obtém as coisas fáceis e depois as mais difíceis, até chegar a um ponto de retornos decrescentes. Então, a percepção que você obtém pode muito bem levar a uma reformulação, iniciando uma nova rodada de acelerações, até que você atinja novamente retornos decrescentes. Agora, este é o ponto em que pode fazer sentido se perguntar se ++iou i++ou for(;;)ou while(1)são mais rápidos: os tipos de perguntas que eu vejo tantas vezes no Stack Overflow.

PS Pode-se perguntar por que não usei um criador de perfil. A resposta é que quase todos esses "problemas" eram um site de chamada de função, que empilha exemplos de maneira precisa. Ainda hoje, os criadores de perfil estão apenas chegando à ideia de que instruções e instruções de chamada são mais importantes para localizar e mais fáceis de corrigir do que funções inteiras.

Na verdade, eu criei um criador de perfil para fazer isso, mas para uma verdadeira intimidade com o que o código está fazendo, não há substituto para acertar os dedos. Não é um problema que o número de amostras seja pequeno, porque nenhum dos problemas encontrados é tão pequeno que é facilmente esquecido.

ADICIONADO: jerryjvl solicitou alguns exemplos. Aqui está o primeiro problema. Consiste em um pequeno número de linhas de código separadas, que levam mais de metade do tempo:

 /* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)

Eles estavam usando o cluster de lista ILST (semelhante a uma classe de lista). Eles são implementados da maneira usual, com "ocultação de informações", significando que os usuários da classe não deveriam ter que se importar com como eles foram implementados. Quando essas linhas foram escritas (de aproximadamente 800 linhas de código), não se pensou na idéia de que elas poderiam ser um "gargalo" (eu odeio essa palavra). Eles são simplesmente a maneira recomendada de fazer as coisas. É fácil dizer, em retrospectiva, que eles deveriam ter sido evitados, mas, na minha experiência, todos os problemas de desempenho são assim. Em geral, é bom tentar evitar a criação de problemas de desempenho. É ainda melhor encontrar e consertar os que são criados, mesmo que "devam ter sido evitados" (em retrospectiva).

Aqui está o segundo problema, em duas linhas separadas:

 /* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)

Essas são listas de construção anexando itens a seus fins. (A correção foi coletar os itens em matrizes e criar as listas de uma só vez.) O interessante é que essas instruções custam apenas (ou seja, estavam na pilha de chamadas) 3/48 do tempo original, portanto, não estavam dentro fato um grande problema no começo . No entanto, depois de remover o primeiro problema, eles custaram 3/20 do tempo e agora eram um "peixe maior". Em geral, é assim que acontece.

Devo acrescentar que este projeto foi destilado de um projeto real no qual ajudei. Nesse projeto, os problemas de desempenho eram muito mais dramáticos (assim como as acelerações), como chamar uma rotina de acesso ao banco de dados dentro de um loop interno para verificar se uma tarefa foi concluída.

REFERÊNCIA ADICIONADA: O código fonte, original e reprojetado, pode ser encontrado em www.ddj.com , para 1993, no arquivo 9311.zip, nos arquivos slug.asc e slug.zip.

EDIT 26/11/2011: Agora existe um projeto SourceForge contendo código-fonte no Visual C ++ e uma descrição detalhada de como foi ajustada. Ele passa apenas pela primeira metade do cenário descrito acima e não segue exatamente a mesma sequência, mas ainda recebe uma aceleração de ordem de magnitude 2-3.


3
Gostaria de ler alguns dos detalhes das etapas descritas acima. É possível incluir alguns fragmentos das otimizações de sabor? (sem fazer o post muito tempo?)
jerryjvl

8
... Também escrevi um livro que está esgotado, por um preço ridículo na Amazon - "Criando melhores aplicativos" ISBN 0442017405. Essencialmente, o mesmo material está no primeiro capítulo.
Mike Dunlavey

3
@ Mike Dunlavey, eu sugiro dizer ao Google que você já o digitalizou. Eles provavelmente já têm um contrato com quem comprou seu editor.
Thorbjørn Ravn Andersen

19
@ Thorbjørn: Só para acompanhar, liguei-me ao GoogleBooks, preenchi todos os formulários e enviei uma cópia impressa para eles. Recebi um e-mail perguntando se eu realmente possuía os direitos autorais. A editora Van Nostrand Reinhold, comprada pela International Thompson, comprada pela Reuters, e quando tento ligar ou enviar um e-mail, é como um buraco negro. Portanto, está no limbo - ainda não tive energia para persegui-la.
precisa saber é o seguinte


188

Sugestões:

  • Pré-calcule em vez de recalcular : quaisquer loops ou chamadas repetidas que contenham cálculos com um intervalo de entradas relativamente limitado, considere fazer uma pesquisa (matriz ou dicionário) que contenha o resultado desse cálculo para todos os valores no intervalo válido de entradas. Em seguida, use uma pesquisa simples dentro do algoritmo.
    Desvantagens : se alguns dos valores pré-calculados forem realmente usados, isso pode piorar as coisas, também a pesquisa pode exigir memória significativa.
  • Não use métodos de biblioteca : a maioria das bibliotecas precisa ser gravada para funcionar corretamente em uma ampla variedade de cenários e executar verificações nulas de parâmetros, etc. não se aplica na circunstância exata em que você o está usando.
    Pontos negativos : escrever código adicional significa mais área de superfície para bugs.
  • Use métodos de biblioteca : para me contradizer, as bibliotecas de idiomas são escritas por pessoas muito mais inteligentes do que você ou eu; as chances são de que eles fizeram isso melhor e mais rápido. Não o implemente, a menos que você possa realmente torná-lo mais rápido (ou seja: sempre meça!)
  • Fraude : em alguns casos, embora exista um cálculo exato para o seu problema, você pode não precisar de 'exato', às vezes uma aproximação pode ser 'boa o suficiente' e muito mais rápida no negócio. Pergunte a si mesmo, realmente importa se a resposta está em 1%? 5%? até 10%?
    Desvantagens : Bem ... a resposta não será exata.

32
A pré-computação nem sempre ajuda, e às vezes pode até prejudicar - se sua tabela de pesquisa for muito grande, poderá prejudicar o desempenho do cache.
Adam Rosenfield

37
Trapacear pode frequentemente ser a vitória. Eu tive um processo de correção de cores que no núcleo era um vetor de 3 pontos pontilhados com uma matriz 3x3. A CPU tinha uma matriz multiplicada em hardware que deixou de fora alguns dos termos cruzados e foi muito rápida em comparação com todas as outras maneiras de fazê-lo, mas suportou apenas matrizes 4x4 e 4 vetores de flutuadores. Alterar o código para transportar o slot extra vazio e converter o cálculo em ponto flutuante do ponto fixo permitiu um resultado um pouco menos preciso, mas muito mais rápido.
RBerteig 30/05/09

6
A trapaça estava no uso de uma multiplicação de matrizes que deixava de fora alguns dos produtos internos, possibilitando a implementação em microcódigo para uma única instrução de CPU que era concluída mais rapidamente do que a sequência equivalente de instruções individuais. É uma trapaça porque não obtém a resposta "correta", apenas uma resposta que é "correta o suficiente".
RBerteig

6
@RBerteig: apenas "correto o suficiente" é uma oportunidade de otimização que a maioria das pessoas sente falta na minha experiência.
Martin Thompson

5
Você nem sempre pode assumir que todo mundo é mais inteligente que você. No final, somos todos profissionais. Você pode supor, no entanto, que uma biblioteca específica que você usa existe e atingiu seu ambiente por causa de sua qualidade; portanto, a gravação dessa biblioteca deve ser muito completa, você não pode fazê-lo apenas porque não é especializado nisso. campo, e você não investe o mesmo tipo de tempo nele. Não porque você é menos inteligente. vamos.
v.oddou 31/07/2014

164

Quando você não puder mais melhorar o desempenho - veja se pode melhorar o desempenho percebido .

Talvez você não consiga acelerar o algoritmo fooCalc, mas muitas vezes existem maneiras de fazer com que seu aplicativo pareça mais responsivo ao usuário.

Alguns exemplos:

  • antecipando o que o usuário vai solicitar e começar a trabalhar nisso antes disso
  • exibindo resultados à medida que entram, em vez de todos de uma vez no final
  • Medidor de progresso preciso

Isso não tornará seu programa mais rápido, mas poderá deixar seus usuários mais felizes com a velocidade que você tem.


27
Uma barra de progresso acelerada no final pode ser percebida como mais rápida do que uma absolutamente precisa. Em "Repensando a barra de progresso" (2007) Harrison, Amento, Kuznetsov e Bell testa vários tipos de barras em um grupo de usuários, além de discutir algumas maneiras de reorganizar as operações para que o progresso seja percebido como mais rápido.
Emil Vikström

9
naxa, a maioria das barras de progresso é falsa porque prever várias etapas amplamente diferentes de um fluxo em uma única porcentagem é difícil ou às vezes impossível. Olhar apenas para todos aqueles bares que fica preso em 99% :-(
Emil Vikström

138

Passo a maior parte da minha vida apenas neste lugar. Os traços gerais são para executar seu criador de perfil e registrá-lo:

  • Falha no cache . O cache de dados é a principal fonte de paralisações na maioria dos programas. Melhore a taxa de acertos do cache reorganizando as estruturas de dados ofensivas para obter uma melhor localidade; empacotar estruturas e tipos numéricos para eliminar bytes desperdiçados (e, portanto, buscas de cache desperdiçadas); pré-busque dados sempre que possível para reduzir paradas.
  • Carregar-hit-lojas . As suposições do compilador sobre o alias de ponteiro e os casos em que os dados são movidos entre conjuntos de registros desconectados via memória podem causar um certo comportamento patológico que faz com que todo o pipeline da CPU seja limpo em uma operação de carregamento. Encontre lugares onde flutuadores, vetores e ints estão sendo convertidos um para o outro e elimine-os. Use __restrictliberalmente para prometer ao compilador sobre alias.
  • Operações microcodificadas . A maioria dos processadores possui algumas operações que não podem ser canalizadas, mas executam uma pequena sub-rotina armazenada na ROM. Exemplos no PowerPC são multiplicar números inteiros, dividir e mudar por quantidade variável. O problema é que o pipeline inteiro para de funcionar enquanto esta operação está em execução. Tente eliminar o uso dessas operações ou, pelo menos, decompô-las em suas operações constituídas por pipeline, para que você possa obter o benefício do envio superescalar de qualquer coisa que o resto do seu programa esteja fazendo.
  • Imprevisíveis do ramo . Estes também esvaziam o pipeline. Encontre casos em que a CPU gaste muito tempo recarregando o canal depois de uma ramificação e use dicas de ramificação, se disponíveis, para conseguir uma previsão correta com mais frequência. Ou, melhor ainda, substitua ramificações por movimentos condicionais sempre que possível, especialmente após operações de ponto flutuante, porque seu tubo é geralmente mais profundo e a leitura dos sinalizadores de condição após fcmp pode causar um estol.
  • Operações sequenciais de ponto flutuante . Faça estes SIMD.

E mais uma coisa que gosto de fazer:

  • Defina seu compilador para exibir listagens de montagem e veja o que ele emite para as funções de ponto de acesso em seu código. Todas essas otimizações inteligentes que "um bom compilador deve poder fazer por você automaticamente"? As chances são de que seu compilador real não as faça. Eu já vi o GCC emitir um código verdadeiramente WTF.

8
Eu uso principalmente Intel VTune e PIX. Não faço ideia se eles podem se adaptar ao C #, mas, realmente, depois de obter a camada de abstração JIT, a maioria dessas otimizações está além do seu alcance, exceto para melhorar a localidade do cache e talvez evitar algumas ramificações.
Crashworks

6
Mesmo assim, verificar a saída pós-JIT pode ajudar a descobrir se há construções que simplesmente não otimizam bem durante o estágio JIT ... a investigação nunca pode prejudicar, mesmo que seja um beco sem saída.
Jerryjvl

5
Eu acho que muitas pessoas, inclusive eu, estariam interessadas neste "conjunto wtf" produzido pelo gcc. Seu soa como um trabalho muito interessante :)
BlueRaja - Danny Pflughoeft

1
Examples on the PowerPC ...<- Ou seja, algumas implementações do PowerPC. O PowerPC é um ISA, não uma CPU.
Billy ONeal

1
@BillyONeal Mesmo no hardware x86 moderno, imul pode parar o pipeline; consulte "Manual de referência da otimização de arquiteturas Intel® 64 e IA-32" §13.3.2.3: "A instrução de multiplicação de números inteiros leva vários ciclos para ser executada. Eles são canalizados de forma que um número de instruções de multiplicação de números inteiros e outra instrução de longa latência possam avançar na No entanto, as instruções de multiplicação de números inteiros bloquearão a emissão de outras instruções de número inteiro de ciclo único devido ao requisito da ordem do programa. " É por isso que geralmente é melhor usar tamanhos de matriz alinhados por palavras e lea.
Crashworks

78

Jogue mais hardware nele!


30
mais hardware nem sempre é uma opção quando você possui um software que deve ser executado em hardware já existente no campo.
29409 Doug T.

76
Não é uma resposta muito útil para alguém que fabrica software de consumo: o cliente não vai querer ouvir você dizer: "compre um computador mais rápido". Especialmente se você estiver escrevendo um software para segmentar algo como um console de videogame.
Crashworks 29/05/09

19
@Crashworks, ou, nesse caso, um sistema incorporado. Quando o último recurso é, finalmente, e o primeiro lote de placas já estão fiado não é o momento para descobrir que você deveria ter usado uma CPU mais rápida, em primeiro lugar ...
RBerteig

71
Certa vez, tive que depurar um programa com um grande vazamento de memória - o tamanho da sua VM cresceu cerca de 1Mb por hora. Um colega brincou que tudo que eu precisava fazer era adicionar memória a uma taxa constante . :)
j_random_hacker

9
Mais hardware: ah sim, a linha de vida do desenvolvedor medíocre. Não sei quantas vezes ouvi "adicionar outra máquina e dobrar a capacidade!"
Olof Forshell 31/03

58

Mais sugestões:

  • Evitar E / S : qualquer E / S (disco, rede, portas etc.) sempre será muito mais lenta que qualquer código que esteja executando cálculos; portanto, livre-se de qualquer E / S desnecessária.

  • Mover E / S antecipadamente : carregue todos os dados necessários para um cálculo antecipado, para que você não tenha repetidas esperas de E / S no núcleo de um algoritmo crítico (e talvez como resultado repetido procura de disco, ao carregar todos os dados em uma ocorrência, pode evitar a busca).

  • Atraso na E / S : não anote seus resultados até o término do cálculo, armazene-os em uma estrutura de dados e despeje-os de uma só vez no final, quando o trabalho duro for concluído.

  • E / S encadeada : para aqueles que ousam o suficiente, combine 'E / S adiantada' ou 'Atraso E / S' com o cálculo real movendo o carregamento para um encadeamento paralelo, para que enquanto estiver carregando mais dados, você possa trabalhar em um cálculo dos dados que você já possui ou enquanto calcula o próximo lote de dados, você pode escrever simultaneamente os resultados do último lote.


3
Observe que "mover o IO para um thread paralelo" deve ser feito como IO assíncrono em muitas plataformas (por exemplo, Windows NT).
Billy ONeal

2
A E / S é realmente um ponto crítico, porque é lento e tem latências enormes, e você pode ficar mais rápido com esse conselho, mas ainda é fundamentalmente defeituoso: os pontos são a latência (que deve ser oculta) e a sobrecarga do syscall ( que precisa ser reduzido, reduzindo o número de chamadas de E / S). O melhor conselho é: use mmap()para entrada, faça madvise()chamadas apropriadas e use aio_write()para escrever grandes blocos de saída (= alguns MiB).
cmaster - restabelecer monica

1
Esta última opção é bastante fácil de implementar em Java, especialmente. Isso proporcionou enormes aumentos de desempenho para aplicativos que eu escrevi. Outro ponto importante (mais do que mover E / S antecipadamente) é torná-lo SEQUENCIAL e E / S de bloco grande. Muitas leituras pequenas são muito mais caras que uma leitura grande, devido ao tempo de busca do disco.
precisa saber é o seguinte

Em um determinado momento, evitei a E / S, movendo temporariamente todos os arquivos para um disco RAM antes do cálculo e movendo-os novamente. Isso está sujo, mas pode ser útil em situações em que você não controla a lógica que faz as chamadas de E / S.
MD

48

Como muitos dos problemas de desempenho envolvem problemas no banco de dados, darei algumas dicas específicas ao ajustar consultas e procedimentos armazenados.

Evite cursores na maioria dos bancos de dados. Evite fazer loop também. Na maioria das vezes, o acesso a dados deve ser baseado em conjunto, não registrado por processamento de registro. Isso inclui a não reutilização de um único procedimento armazenado de registro quando você deseja inserir 1.000.000 de registros de uma só vez.

Nunca use select *, apenas retorne os campos que você realmente precisa. Isso é especialmente verdadeiro se houver junções, pois os campos de junção serão repetidos e, portanto, causarão uma carga desnecessária no servidor e na rede.

Evite o uso de subconsultas correlacionadas. Use junções (incluindo junções a tabelas derivadas sempre que possível) (eu sei que isso é verdade para o Microsoft SQL Server, mas teste o conselho ao usar um back-end diferente).

Índice, índice, índice. E atualize essas estatísticas, se aplicável ao seu banco de dados.

Torne a consulta sargável . O significado evita coisas que tornam impossível o uso de índices, como o uso de um curinga no primeiro caractere de uma cláusula like ou de uma função na junção ou como a parte esquerda de uma instrução where.

Use tipos de dados corretos. É mais rápido fazer a matemática da data em um campo de data do que tentar converter um tipo de dados da cadeia de caracteres em um tipo de dados da data e, em seguida, fazer o cálculo.

Nunca coloque um loop de qualquer tipo em um gatilho!

A maioria dos bancos de dados tem uma maneira de verificar como a execução da consulta será feita. No Microsoft SQL Server, isso é chamado de plano de execução. Verifique os primeiros para ver onde estão as áreas problemáticas.

Considere com que frequência a consulta é executada e quanto tempo leva para executar ao determinar o que precisa ser otimizado. Às vezes, você pode obter mais desempenho com um pequeno ajuste em uma consulta que é executada milhões de vezes por dia do que com o tempo gasto em uma consulta de execução longa que é executada apenas uma vez por mês.

Use algum tipo de ferramenta de criação de perfil para descobrir o que realmente está sendo enviado para e do banco de dados. Lembro-me de uma vez no passado em que não conseguimos descobrir por que a página era tão lenta para carregar quando o procedimento armazenado era rápido e descobri através de criação de perfil que a página da Web estava solicitando a consulta muitas vezes, em vez de uma vez.

O criador de perfil também ajudará você a descobrir quem está bloqueando quem. Algumas consultas executadas rapidamente enquanto são executadas sozinhas podem ficar muito lentas devido a bloqueios de outras consultas.


29

O fator limitante mais importante atualmente é a largura de banda de memória limitada . Os multicores estão apenas piorando isso, pois a largura de banda é compartilhada entre os núcleos. Além disso, a área limitada de chips dedicada à implementação de caches também é dividida entre núcleos e threads, agravando ainda mais esse problema. Finalmente, a sinalização entre chips necessária para manter os diferentes caches coerentes também aumenta com um número maior de núcleos. Isso também adiciona uma penalidade.

Esses são os efeitos que você precisa gerenciar. Às vezes, através do microgerenciamento do seu código, mas, às vezes, pela consideração cuidadosa e refatoração.

Muitos comentários já mencionam o código amigável do cache. Existem pelo menos dois sabores distintos disso:

  • Evite latências de busca de memória.
  • Menor pressão do barramento de memória (largura de banda).

O primeiro problema especificamente tem a ver com tornar seus padrões de acesso a dados mais regulares, permitindo que o pré-buscador de hardware funcione com eficiência. Evite a alocação dinâmica de memória, que espalha seus objetos de dados na memória. Use recipientes lineares em vez de listas, hashes e árvores vinculadas.

O segundo problema tem a ver com a melhoria da reutilização de dados. Altere seus algoritmos para trabalhar em subconjuntos de dados que se encaixam no cache disponível e reutilize esses dados o máximo possível enquanto ainda estiverem no cache.

Empacotar dados com mais força e garantir que você use todos os dados nas linhas de cache nos hot loops ajudará a evitar esses outros efeitos e permitirá ajustar dados mais úteis no cache.


25
  • Em que hardware você está executando? Você pode usar otimizações específicas da plataforma (como vetorização)?
  • Você pode obter um compilador melhor? Por exemplo, mudar do GCC para a Intel?
  • Você pode fazer seu algoritmo rodar em paralelo?
  • Você pode reduzir as falhas de cache reorganizando os dados?
  • Você pode desativar as declarações?
  • Otimize micro para seu compilador e plataforma. No estilo de "em um if / else, coloque a declaração mais comum em primeiro lugar"

4
Deveria ser "alternar do GCC para o LLVM" :) #
3060 Zifre

4
Você pode fazer seu algoritmo rodar em paralelo? - o inverso também se aplica
justin

4
Verdade que, reduzindo a quantidade de tópicos pode ser igualmente uma boa otimização
Johan Kotliński

re: micro-otimização: se você verificar a saída asm do compilador, poderá ajustar a fonte para segurá-la manualmente, produzindo melhor asm. Consulte Por que esse código C ++ é mais rápido que meu assembly escrito à mão para testar a conjectura de Collatz? para saber mais sobre como ajudar ou derrotar o compilador no x86 moderno.
22626 Peter Cordes #

17

Embora eu goste da resposta de Mike Dunlavey, na verdade é uma ótima resposta com exemplo de apoio, acho que poderia ser expresso de maneira muito simples:

Descubra o que leva mais tempo primeiro e entenda o porquê.

É o processo de identificação dos porcos do tempo que ajuda a entender onde você deve refinar seu algoritmo. Essa é a única resposta independente de linguagem que posso encontrar para um problema que já deveria estar totalmente otimizado. Também presumindo que você queira ser independente da arquitetura em sua busca por velocidade.

Portanto, embora o algoritmo possa ser otimizado, sua implementação pode não ser. A identificação permite que você saiba qual parte é qual: algoritmo ou implementação. Portanto, o que mais dificulta o tempo é o seu principal candidato para a revisão. Mas como você diz que deseja extrair os últimos%, você também pode examinar as partes menores, as que você não examinou tão atentamente a princípio.

Por fim, um pouco de tentativa e erro com números de desempenho de diferentes maneiras de implementar a mesma solução, ou algoritmos potencialmente diferentes, pode trazer informações que ajudam a identificar desperdiçadores de tempo e poupadores de tempo.

HPH, também.


16

Você provavelmente deve considerar a "perspectiva do Google", ou seja, determinar como seu aplicativo pode se tornar amplamente paralelo e simultâneo, o que inevitavelmente também significará, em algum momento, a distribuição do seu aplicativo por diferentes máquinas e redes, para que ele possa escalar idealmente quase linearmente com o hardware que você joga nele.

Por outro lado, o pessoal do Google também é conhecido por disponibilizar muita mão de obra e recursos para resolver alguns dos problemas em projetos, ferramentas e infraestrutura que estão usando, como por exemplo , otimização de todo o programa para o gcc , com uma equipe dedicada de engenheiros. invadir internamente o gcc para prepará-lo para cenários de casos de uso típicos do Google.

Da mesma forma, criar um perfil de um aplicativo não significa mais simplesmente criar um perfil do código do programa, mas também de todos os seus sistemas e infraestrutura (redes, comutadores, servidores, matrizes RAID) para identificar redundâncias e potencial de otimização do ponto de vista do sistema.


15
  • Rotinas em linha (eliminam chamada / retorno e envio de parâmetros)
  • Tente eliminar testes / opções com consultas na tabela (se forem mais rápidas)
  • Desenrole os loops (dispositivo de Duff) até o ponto em que eles apenas cabem no cache da CPU
  • Localize o acesso à memória para não danificar seu cache
  • Localize cálculos relacionados se o otimizador ainda não estiver fazendo isso
  • Elimine invariantes de loop se o otimizador já não estiver fazendo isso

2
O dispositivo do IIRC Duff raramente é mais rápido. Somente quando a op é muito curto (como uma única expressão pequena matemática)
BCS

12
  • Quando você chega ao ponto de usar algoritmos eficientes, é uma questão do que você precisa de mais velocidade ou memória . Use o cache para "pagar" na memória para obter mais velocidade ou use cálculos para reduzir o espaço ocupado pela memória.
  • Se possível (e mais econômico), jogue o hardware no problema - CPU mais rápida, mais memória ou HD poderá resolver o problema mais rapidamente do que tentar codificá-lo.
  • Use paralelização se possível - execute parte do código em vários threads.
  • Use a ferramenta certa para o trabalho . algumas linguagens de programação criam código mais eficiente, usando código gerenciado (por exemplo, Java / .NET) acelera o desenvolvimento, mas linguagens de programação nativas criam código de execução mais rápida.
  • Micro otimizar . Somente foram aplicáveis. Você pode usar a montagem otimizada para acelerar pequenos pedaços de código. Usar otimizações de SSE / vetores nos lugares certos pode aumentar muito o desempenho.

12

Dividir e conquistar

Se o conjunto de dados que está sendo processado for muito grande, faça um loop sobre pedaços dele. Se você fez seu código corretamente, a implementação deve ser fácil. Se você tem um programa monolítico, agora sabe melhor.


9
+1 no som do "mata-moscas" que ouvi ao ler a última frase.
Bryan Boettcher

11

Antes de tudo, como mencionado em várias respostas anteriores, aprenda o que afeta seu desempenho - é memória ou processador, rede ou banco de dados ou outra coisa. Dependendo disso ...

  • ... se for memória - encontre um dos livros escritos há muito tempo por Knuth, uma das séries "The Art of Computer Programming". Provavelmente, trata-se de classificação e pesquisa - se minha memória estiver errada, você terá que descobrir em que ele fala sobre como lidar com o armazenamento lento de dados em fita. Transforme mentalmente seu aperto de memória / fita par de em seu par de cache / memória principal (ou em par de cache L1 / L2), respectivamente. Estude todos os truques que ele descreve - se você não encontrar algo que resolva seu problema, contrate um cientista da computação profissional para realizar uma pesquisa profissional. Se seu problema de memória ocorrer por acaso com a FFT (o cache falha em índices com inversão de bits ao fazer borboletas radix-2), não contrate um cientista. Em vez disso, otimize manualmente as passagens uma a uma até que você ' até os últimos por cento, certo? Se forem poucos , você provavelmente vencerá.

  • ... se for processador - mude para a linguagem assembly. Estudo da especificação do processador - o que leva carrapatos , VLIW, SIMD. As chamadas de função são provavelmente devoradoras de carrapatos substituíveis. Aprenda transformações de loop - pipeline, desenrole. Múltiplas e divisões podem ser substituíveis / interpoladas com deslocamento de bits (multiplicações por números inteiros pequenos podem ser substituíveis por adições). Tente truques com dados mais curtos - se você tiver sorte, uma instrução com 64 bits pode ser substituída por duas em 32 ou até 4 em 16 ou 8 em 8 bits. Tente também mais tempodados - por exemplo, seus cálculos de flutuação podem ficar mais lentos que o dobro em um processador específico. Se você tem coisas trigonométricas, lute com tabelas pré-calculadas; Lembre-se também de que o seno de pequeno valor pode ser substituído por esse valor se a perda de precisão estiver dentro dos limites permitidos.

  • ... se for em rede - pense em compactar os dados que você passar por eles. Substitua a transferência XML por binária. Protocolos de estudo. Experimente o UDP em vez do TCP se você conseguir lidar com a perda de dados.

  • ... se for banco de dados, vá a qualquer fórum de banco de dados e peça conselhos. Grade de dados na memória, otimização do plano de consulta etc etc etc.

HTH :)


9

Armazenamento em cache! Uma maneira barata (no esforço do programador) de tornar quase tudo mais rápido é adicionar uma camada de abstração de cache a qualquer área de movimentação de dados do seu programa. Seja E / S ou apenas passando / criação de objetos ou estruturas. Muitas vezes, é fácil adicionar caches às classes de fábrica e aos leitores / gravadores.

Às vezes, o cache não ganha muito, mas é um método fácil adicionar apenas o cache e desabilitá-lo onde não ajudar. Eu sempre achei isso para obter um desempenho enorme sem ter que micro-analisar o código.


8

Eu acho que isso já foi dito de uma maneira diferente. Porém, ao lidar com um algoritmo intensivo de processador, você deve simplificar tudo dentro do loop mais interno às custas de todo o resto.

Isso pode parecer óbvio para alguns, mas é algo em que tento me concentrar, independentemente do idioma em que estou trabalhando. Se você está lidando com loops aninhados, por exemplo, e encontra uma oportunidade de reduzir algum código em um nível, pode, em alguns casos, acelerar drasticamente seu código. Como outro exemplo, há pequenas coisas em que pensar, como trabalhar com números inteiros em vez de variáveis ​​de ponto flutuante sempre que possível e usar multiplicação em vez de divisão sempre que possível. Novamente, essas são coisas que devem ser consideradas para o seu loop mais interno.

Às vezes, você pode encontrar o benefício de executar suas operações matemáticas em um número inteiro dentro do loop interno e reduzi-lo a uma variável de ponto flutuante com a qual você pode trabalhar posteriormente. Esse é um exemplo de sacrifício de velocidade em uma seção para melhorar a velocidade em outra, mas em alguns casos o pagamento pode valer a pena.


8

Passei algum tempo trabalhando na otimização de sistemas de negócios cliente / servidor operando em redes de baixa largura de banda e longa latência (por exemplo, satélite, remoto, offshore) e consegui algumas melhorias dramáticas no desempenho com um processo bastante repetitivo.

  • Medida : Comece entendendo a capacidade e a topologia subjacentes da rede. Conversando com as pessoas relevantes da rede nos negócios e utilize ferramentas básicas, como ping e traceroute, para estabelecer (no mínimo) a latência da rede de cada local do cliente, durante períodos operacionais típicos. Em seguida, faça medições precisas de tempo de funções específicas do usuário final que exibem os sintomas problemáticos. Registre todas essas medidas, juntamente com seus locais, datas e horas. Considere criar a funcionalidade de "teste de desempenho de rede" do usuário final em seu aplicativo cliente, permitindo que seus usuários avançados participem do processo de melhoria; capacitá-los dessa maneira pode ter um enorme impacto psicológico quando você lida com usuários frustrados por um sistema com desempenho insatisfatório.

  • Analisar : Usando todo e qualquer método de registro disponível para estabelecer exatamente quais dados estão sendo transmitidos e recebidos durante a execução das operações afetadas. Idealmente, seu aplicativo pode capturar dados transmitidos e recebidos pelo cliente e pelo servidor. Se estes incluem também carimbos de data e hora, melhor ainda. Se o registro suficiente não estiver disponível (por exemplo, sistema fechado ou incapacidade de implantar modificações em um ambiente de produção), use um sniffer de rede e verifique se realmente entende o que está acontecendo no nível da rede.

  • Cache : procure casos em que dados estáticos ou alterados com pouca frequência estejam sendo transmitidos repetidamente e considere uma estratégia de cache apropriada. Exemplos típicos incluem valores de "lista de seleção" ou outras "entidades de referência", que podem ser surpreendentemente grandes em alguns aplicativos de negócios. Em muitos casos, os usuários podem aceitar que eles devem reiniciar ou atualizar o aplicativo para atualizar dados atualizados com pouca frequência, especialmente se ele puder economizar um tempo significativo da exibição dos elementos da interface do usuário comumente usados. Certifique-se de entender o comportamento real dos elementos de armazenamento em cache já implementados - muitos métodos comuns de armazenamento em cache (por exemplo, HTTP ETag) ainda exigem uma viagem de ida e volta à rede para garantir consistência e, onde a latência da rede é cara, você pode evitá-lo completamente com uma abordagem de armazenamento em cache diferente.

  • Paralelamente : procure transações seqüenciais que não precisam ser emitidas logicamente estritamente em sequência e refaça o trabalho do sistema para emiti-las em paralelo. Lidei com um caso em que uma solicitação de ponta a ponta tinha um atraso de rede inerente de ~ 2s, o que não era um problema para uma única transação, mas quando eram necessárias 6 viagens de ida e volta sequenciais 2s antes que o usuário recuperasse o controle do aplicativo cliente. , tornou-se uma enorme fonte de frustração. Descobrir que essas transações eram de fato independentes permitiu que fossem executadas em paralelo, reduzindo o atraso do usuário final para muito próximo ao custo de uma única viagem de ida e volta.

  • Combinar : onde as solicitações sequenciais devem ser executadas sequencialmente, procure oportunidades para combiná-las em uma única solicitação mais abrangente. Exemplos típicos incluem a criação de novas entidades, seguidos de solicitações para relacionar essas entidades a outras entidades existentes.

  • Compactar : procure oportunidades para alavancar a compactação da carga útil, substituindo um formulário textual por um binário ou usando a tecnologia de compactação real. Muitas pilhas modernas de tecnologia (isto é, dentro de uma década) suportam isso quase de forma transparente, portanto, verifique se está configurado. Muitas vezes, fico surpreso com o impacto significativo da compactação, onde parecia claro que o problema era fundamentalmente latência e não largura de banda, descobrindo depois que permitia que a transação cabesse em um único pacote ou evitava a perda de pacotes e, portanto, tinha um tamanho excessivo. impacto no desempenho.

  • Repita : volte ao início e meça novamente suas operações (nos mesmos locais e horários) com as melhorias implementadas, registre e relate seus resultados. Como em toda otimização, alguns problemas podem ter sido resolvidos expondo outros que agora dominam.

Nas etapas acima, concentro-me no processo de otimização relacionado ao aplicativo, mas é claro que você deve garantir que a própria rede subjacente esteja configurada da maneira mais eficiente para oferecer suporte ao seu aplicativo também. Envolva os especialistas em rede nos negócios e determine se eles podem aplicar melhorias de capacidade, QoS, compactação de rede ou outras técnicas para resolver o problema. Geralmente, eles não entendem as necessidades de seu aplicativo, por isso é importante que você esteja preparado (após a etapa Analisar) para discuti-lo com eles e também faça um argumento comercial quanto a quaisquer custos que você solicitará . Encontrei casos em que a configuração de rede incorreta fazia com que os dados dos aplicativos fossem transmitidos por um link lento por satélite, e não por um link terrestre, simplesmente porque estava usando uma porta TCP que não era "bem conhecida" pelos especialistas em redes; obviamente, a correção de um problema como esse pode ter um impacto dramático no desempenho, sem a necessidade de alterações no código ou na configuração do software.


7

Muito difícil dar uma resposta genérica para esta pergunta. Realmente depende do domínio do problema e da implementação técnica. Uma técnica geral que é bastante neutra em termos de idioma: identifique pontos de acesso de código que não podem ser eliminados e otimize manualmente o código do assembler.


7

Os últimos% são uma coisa muito dependente da CPU e do aplicativo ....

  • As arquiteturas de cache diferem, alguns chips têm RAM no chip, você pode mapear diretamente, os ARMs (às vezes) têm uma unidade vetorial, o SH4 é um código operacional da matriz útil. Existe uma GPU - talvez um shader seja o caminho a seguir. Os TMS320 são muito sensíveis a ramificações dentro de loops (separe os loops e mova as condições para fora, se possível).

A lista continua .... Mas esse tipo de coisa realmente é o último recurso ...

Crie para x86 e execute Valgrind / Cachegrind no código para criar um perfil de desempenho adequado. Ou o CCStudio da Texas Instruments tem um bom perfil. Então você realmente saberá onde se concentrar ...


7

Did you know that a CAT6 cable is capable of 10x better shielding off extrenal inteferences than a default Cat5e UTP cable?

Para qualquer projeto não offline, apesar de ter o melhor software e o melhor hardware, se sua taxa de transferência for fraca, essa linha fina irá espremer dados e causar atrasos, embora em milissegundos ... mas se você estiver falando das últimas quedas , são algumas gotas obtidas, 24 horas por dia, 7 dias por semana para qualquer pacote enviado ou recebido.


7

Não é tão profundo ou complexo quanto as respostas anteriores, mas aqui vai: (estas são mais para iniciantes / intermediários)

  • óbvio: seco
  • executar loops para trás para que você esteja sempre comparando a 0 em vez de uma variável
  • use operadores bit a bit sempre que puder
  • quebrar código repetitivo em módulos / funções
  • objetos de cache
  • variáveis ​​locais têm uma ligeira vantagem de desempenho
  • limitar a manipulação de strings o máximo possível

4
Sobre o loop reverso: sim, a comparação para o final do loop será mais rápida. Normalmente, você usa a variável para indexar na memória e acessá-la invertida pode ser contraproducente devido a falhas frequentes no cache (sem pré-busca).
Andreas Reiff

1
AFAIK, na maioria dos casos, qualquer otimizador razoável funciona bem com loops, sem que o programador precise executar explicitamente o inverso. Ou o otimizador inverte o próprio loop ou tem outra maneira igualmente boa. Notei uma saída ASM idêntica para loops (reconhecidamente relativamente simples) escritos tanto ascendente quanto máximo e descendente contra 0. Claro, meus dias de Z80 têm o hábito de escrever reflexivamente loops para trás, mas suspeito que mencioná-lo para iniciantes geralmente é um arenque vermelho / otimização prematura, quando código legível e aprender práticas mais importantes devem ser prioridades.
underscore_d

Pelo contrário, executar um loop para trás será mais lento em idiomas de nível inferior, porque em uma guerra entre comparação com zero mais subtração adicional versus uma comparação inteira única, a comparação inteira única é mais rápida. Em vez de diminuir, você pode ter um ponteiro para o endereço inicial na memória e um ponteiro para o endereço final na memória. Em seguida, aumente o ponteiro inicial até que seja igual ao ponteiro final. Isso eliminará a operação de deslocamento de memória extra no código de montagem, provando muito mais desempenho.
Jack Giffin

5

Impossível dizer. Depende da aparência do código. Se pudermos assumir que o código já existe, podemos simplesmente olhar para ele e descobrir a partir disso, como otimizá-lo.

Melhor localidade do cache, desenrolamento de loop, Tente eliminar longas cadeias de dependência, para obter um melhor paralelismo no nível das instruções. Prefira movimentos condicionais sobre ramificações quando possível. Explore as instruções do SIMD quando possível.

Entenda o que seu código está fazendo e o hardware em que está sendo executado. Torna-se bastante simples determinar o que você precisa fazer para melhorar o desempenho do seu código. Esse é realmente o único conselho verdadeiramente geral em que consigo pensar.

Bem, isso e "Mostre o código no SO e peça conselhos de otimização para esse trecho de código específico".


5

Se um hardware melhor é uma opção, definitivamente faça isso. De outra forma

  • Verifique se você está usando as melhores opções de compilador e vinculador.
  • Se a rotina do ponto de acesso estiver em uma biblioteca diferente para o chamador frequente, considere movê-lo ou cloná-lo no módulo de chamadores. Elimina parte da sobrecarga de chamada e pode melhorar as ocorrências de cache (veja como o AIX vincula strcpy () estaticamente em objetos compartilhados vinculados separadamente). Obviamente, isso também pode diminuir os acertos do cache, e é por isso que uma medida.
  • Veja se existe alguma possibilidade de usar uma versão especializada da rotina de hotspot. A desvantagem é mais de uma versão para manter.
  • Olhe para o montador. Se você acha que poderia ser melhor, considere por que o compilador não descobriu isso e como você pode ajudar o compilador.
  • Considere: você realmente está usando o melhor algoritmo? É o melhor algoritmo para o tamanho da sua entrada?

Eu acrescentaria ao seu primeiro par .: não se esqueça de desativar todas as informações de depuração nas opções do compilador .
varnie

5

O caminho do Google é uma opção "Coloque em cache. Sempre que possível, não toque no disco"


5

Aqui estão algumas técnicas de otimização rápidas e sujas que eu uso. Considero isso uma otimização de 'primeira passagem'.

Saiba onde é gasto o tempo Descubra exatamente o que está demorando. É arquivo IO? É hora da CPU? É a rede? É o banco de dados? É inútil otimizar para E / S se esse não for o gargalo.

Conheça o seu ambiente Saber onde otimizar normalmente depende do ambiente de desenvolvimento. No VB6, por exemplo, passar por referência é mais lento que passar por valor, mas em C e C ++, por referência é muito mais rápido. Em C, é razoável tentar algo e fazer algo diferente se um código de retorno indicar uma falha, enquanto no Dot Net, as exceções de captura são muito mais lentas do que procurar uma condição válida antes de tentar.

Índices Crie índices nos campos do banco de dados consultados com freqüência. Você quase sempre pode trocar espaço por velocidade.

Evitar pesquisas Dentro do loop para ser otimizado, evito ter que fazer pesquisas. Encontre o deslocamento e / ou índice fora do loop e reutilize os dados dentro.

Minimize as E / S tente projetar de uma maneira que reduz o número de vezes que você precisa ler ou gravar, especialmente em uma conexão em rede

Reduzir abstrações Quanto mais camadas de abstração o código tiver que trabalhar, mais lento será. Dentro do loop crítico, reduza abstrações (por exemplo, revele métodos de nível inferior que evitam código extra)

Gerar Threads para projetos com uma interface de usuário, gerando um novo thread para realizar tarefas mais lentas faz com que o aplicativo pareça mais responsivo, embora não seja.

Pré-processo Geralmente, você pode trocar espaço por velocidade. Se houver cálculos ou outras operações intensas, verifique se você pode pré-calcular algumas informações antes de entrar no ciclo crítico.


5

Se você tiver um monte de matemática de ponto flutuante altamente paralela, especialmente com precisão única, tente descarregá-la para um processador gráfico (se houver) usando OpenCL ou (para chips NVidia) CUDA. As GPUs têm imenso poder de computação de ponto flutuante em seus shaders, o que é muito maior que o de uma CPU.


5

Adicionando esta resposta, já que não a vi incluída em todas as outras.

Minimize a conversão implícita entre tipos e sinal:

Isso se aplica ao C / C ++, pelo menos, mesmo se você já pensa que está livre de conversões - às vezes é bom testar a adição de avisos do compilador em torno de funções que exigem desempenho, principalmente atenção a conversões em loops.

Específico do GCC: você pode testar isso adicionando alguns pragmas detalhados ao seu código,

#ifdef __GNUC__
#  pragma GCC diagnostic push
#  pragma GCC diagnostic error "-Wsign-conversion"
#  pragma GCC diagnostic error "-Wdouble-promotion"
#  pragma GCC diagnostic error "-Wsign-compare"
#  pragma GCC diagnostic error "-Wconversion"
#endif

/* your code */

#ifdef __GNUC__
#  pragma GCC diagnostic pop
#endif

Vi casos em que você pode obter uma aceleração de alguns por cento, reduzindo as conversões geradas por avisos como este.

Em alguns casos, tenho um cabeçalho com avisos estritos que mantenho incluídos para evitar conversões acidentais; no entanto, isso é uma troca, já que você pode adicionar muitos elencos para conversões intencionais silenciosas, o que pode tornar o código mais confuso por um mínimo de tempo. ganhos.


É por isso que gosto disso no OCaml, a conversão entre tipos numéricos deve ser xplicit.
Gaius

@Gaius fair point - mas, em muitos casos, mudar de idioma não é uma escolha realista. Como o C / C ++ é tão amplamente utilizado, é útil poder torná-los mais rigorosos, mesmo que sejam específicos ao compilador.
ideasman42

4

Às vezes, alterar o layout dos seus dados pode ajudar. Em C, você pode alternar de uma matriz ou estruturas para uma estrutura de matrizes ou vice-versa.


4

Ajuste o sistema operacional e a estrutura.

Pode parecer um exagero, mas pense assim: Sistemas operacionais e estruturas são projetados para fazer muitas coisas. Seu aplicativo faz apenas coisas muito específicas. Se você conseguir fazer com que o sistema operacional faça exatamente o que seu aplicativo precisa e faça com que ele entenda como a estrutura (php, .net, java) funciona, você poderá melhorar muito seu hardware.

O Facebook, por exemplo, mudou algumas coisas no nível do kernel no Linux, mudou o funcionamento do memcached (por exemplo, eles escreveram um proxy memcached e usaram o udp em vez do tcp ).

Outro exemplo para isso é o Window2008. O Win2K8 possui uma versão em que você pode instalar apenas o sistema operacional básico necessário para executar aplicativos X (por exemplo, aplicativos da Web, aplicativos de servidor). Isso reduz grande parte da sobrecarga que o sistema operacional possui nos processos em execução e oferece melhor desempenho.

Claro, você sempre deve usar mais hardware como o primeiro passo ...


2
Essa seria uma abordagem válida após todas as outras abordagens falharem, ou se um recurso específico do SO ou da Estrutura fosse responsável por uma redução acentuada no desempenho, mas o nível de conhecimento e controle necessário para fazer isso pode não estar disponível para todos os projetos.
Andrew Neely
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.