Muitos anos atrás, os compiladores C não eram particularmente inteligentes. Para contornar o problema, K&R inventou a palavra-chave register , para sugerir ao compilador que talvez fosse uma boa ideia manter essa variável em um registro interno. Eles também fizeram o operador terciário para ajudar a gerar um código melhor.
Com o passar do tempo, os compiladores amadureceram. Eles se tornaram muito espertos em sua análise de fluxo, permitindo-lhes tomar melhores decisões sobre quais valores manter nos registros do que você poderia fazer. A palavra-chave de registro tornou-se sem importância.
FORTRAN pode ser mais rápido que C para alguns tipos de operações, devido a problemas de alias . Em teoria, com uma codificação cuidadosa, pode-se contornar essa restrição para permitir que o otimizador gere código mais rápido.
Quais práticas de codificação estão disponíveis que podem permitir que o compilador / otimizador gere código mais rápido?
- Identificar a plataforma e o compilador que você usa, seria apreciado.
- Por que a técnica parece funcionar?
- O código de amostra é incentivado.
Aqui está uma questão relacionada
[Editar] Esta questão não é sobre o processo geral de definir o perfil e otimizar. Suponha que o programa foi escrito corretamente, compilado com otimização total, testado e colocado em produção. Pode haver construções em seu código que impeçam o otimizador de fazer o melhor trabalho possível. O que você pode fazer para refatorar que irá remover essas proibições e permitir que o otimizador gere um código ainda mais rápido?
[Editar] Link relacionado ao deslocamento
fonte
register
na verdade, tornou o código sensível ao desempenho mais portátil ao combater compiladores ruins.Respostas:
Grave em variáveis locais e não em argumentos de saída! Isso pode ser uma grande ajuda para contornar lentidões de aliasing. Por exemplo, se o seu código for
o compilador não sabe que foo1! = barOut e, portanto, precisa recarregar foo1 a cada vez que passar pelo loop. Ele também não pode ler foo2 [i] até que a gravação em barOut seja concluída. Você pode começar a mexer com indicadores restritos, mas é tão eficaz (e muito mais claro) fazer isso:
Parece bobo, mas o compilador pode ser muito mais inteligente ao lidar com a variável local, já que ela não pode se sobrepor na memória a nenhum dos argumentos. Isso pode ajudá-lo a evitar o temido load-hit-store (mencionado por Francis Boivin neste tópico).
fonte
Aqui está uma prática de codificação para ajudar o compilador a criar código rápido - qualquer linguagem, qualquer plataforma, qualquer compilador, qualquer problema:
Você não usar todos os truques inteligentes que a força, ou mesmo incentivar, o compilador para colocar as variáveis na memória (incluindo cache e registos) como achar melhor. Escreva primeiro um programa que seja correto e sustentável.
Em seguida, analise seu código.
Então, e somente então, você pode querer começar a investigar os efeitos de dizer ao compilador como usar a memória. Faça uma mudança de cada vez e meça seu impacto.
Espere ficar desapontado e ter que trabalhar muito mesmo para pequenas melhorias de desempenho. Compiladores modernos para linguagens maduras como Fortran e C são muito, muito bons. Se você leu o relato de um 'truque' para obter melhor desempenho do código, tenha em mente que os escritores do compilador também leram sobre ele e, se valer a pena, provavelmente o implementaram. Eles provavelmente escreveram o que você leu em primeiro lugar.
fonte
&
vs.%
para potências de dois (raramente, ou nunca, otimizado, mas pode ter impactos significativos no desempenho). Se você ler um truque de desempenho, a única maneira de saber se funciona é fazer a mudança e medir o impacto. Nunca assuma que o compilador otimizará algo para você.n
, substitui gcc% n
com& (n-1)
mesmo quando otimização está desativado . Isso não é exatamente "raramente, se nunca" ...A ordem em que você atravessa a memória pode ter impactos profundos no desempenho, e os compiladores não são muito bons em descobrir e consertar isso. Você deve estar atento às questões de localidade do cache ao escrever o código, se você se preocupa com o desempenho. Por exemplo, matrizes bidimensionais em C são alocadas no formato de linha principal. Percorrer matrizes no formato principal da coluna tenderá a fazer com que você tenha mais perdas de cache e a tornar seu programa mais limitado à memória do que ao processador:
fonte
-floop-interchange
que inverterá um loop interno e externo se o otimizador considerar lucrativo.Otimizações genéricas
Aqui estão algumas das minhas otimizações favoritas. Na verdade, aumentei os tempos de execução e reduzi os tamanhos dos programas usando isso.
Declarar pequenas funções como
inline
ou macrosCada chamada a uma função (ou método) incorre em sobrecarga, como colocar variáveis na pilha. Algumas funções também podem gerar uma sobrecarga no retorno. Uma função ou método ineficiente tem menos instruções em seu conteúdo do que a sobrecarga combinada. Esses são bons candidatos para inlining, seja como
#define
macros ouinline
funções. (Sim, eu sei queinline
é apenas uma sugestão, mas neste caso eu considero um lembrete para o compilador).Remova o código morto e redundante
Se o código não for usado ou não contribuir para o resultado do programa, livre-se dele.
Simplifique o design de algoritmos
Certa vez, removi muito código de montagem e tempo de execução de um programa, escrevendo a equação algébrica que ele estava calculando e, em seguida, simplifiquei a expressão algébrica. A implementação da expressão algébrica simplificada ocupou menos espaço e tempo do que a função original.
Loop Unrolling
Cada loop tem uma sobrecarga de incremento e verificação de término. Para obter uma estimativa do fator de desempenho, conte o número de instruções no overhead (mínimo 3: incremento, verificação, vá para o início do loop) e divida pelo número de instruções dentro do loop. Quanto menor o número, melhor.
Editar: forneça um exemplo de desenrolamento de loop Antes:
Após desenrolar:
Com essa vantagem, um benefício secundário é obtido: mais instruções são executadas antes que o processador precise recarregar o cache de instruções.
Tive resultados incríveis quando desenrolei um loop para 32 declarações. Esse foi um dos gargalos, pois o programa teve que calcular uma soma de verificação em um arquivo de 2 GB. Essa otimização combinada com a leitura de blocos melhorou o desempenho de 1 hora para 5 minutos. O desenrolamento de loops também forneceu um desempenho excelente em linguagem assembly, mas
memcpy
foi muito mais rápido que o compiladormemcpy
. - TMRedução de
if
declaraçõesOs processadores detestam desvios ou saltos, uma vez que força o processador a recarregar sua fila de instruções.
Aritmética booleana ( editado: formato de código aplicado ao fragmento de código, exemplo adicionado)
Converta
if
instruções em atribuições booleanas. Alguns processadores podem executar instruções condicionalmente sem ramificação:O curto-circuito do operador lógico AND (
&&
) impede a execução dos testes se ostatus
forfalse
.Exemplo:
Alocação de variável de fator fora dos loops
Se uma variável for criada rapidamente dentro de um loop, mova a criação / alocação para antes do loop. Na maioria dos casos, a variável não precisa ser alocada durante cada iteração.
Fatore as expressões constantes fora dos loops
Se um cálculo ou valor de variável não depende do índice do loop, mova-o para fora (antes) do loop.
I / O em blocos
Leia e grave dados em grandes blocos (blocos). Quanto maior melhor. Por exemplo, ler um octeto por vez é menos eficiente do que ler 1.024 octetos com uma leitura.
Exemplo:
A eficiência desta técnica pode ser demonstrada visualmente. :-)
Não use
printf
família para dados constantesDados constantes podem ser produzidos usando uma gravação de bloco. A gravação formatada perderá tempo examinando o texto em busca de caracteres de formatação ou processando comandos de formatação. Veja o exemplo de código acima.
Formate na memória e depois escreva
Formate em uma
char
matriz usando vários esprintf
, em seguida, usefwrite
. Isso também permite que o layout de dados seja dividido em "seções constantes" e seções variáveis. Pense em mala direta .Declare texto constante (literais de string) como
static const
Quando as variáveis são declaradas sem o
static
, alguns compiladores podem alocar espaço na pilha e copiar os dados da ROM. Essas são duas operações desnecessárias. Isso pode ser corrigido usando ostatic
prefixo.Por último, o código como o compilador faria
Às vezes, o compilador pode otimizar várias pequenas instruções melhor do que uma versão complicada. Além disso, escrever código para ajudar a otimizar o compilador também ajuda. Se eu quiser que o compilador use instruções especiais de transferência de bloco, vou escrever um código que parece que deve usar as instruções especiais.
fonte
fprintf
formatos em um buffer separado gerem a saída do buffer. Um simplificado (para uso de memória)fprintf
produziria todo o texto não formatado, em seguida, formataria e geraria, e repetiria até que toda a string de formato fosse processada, fazendo assim 1 chamada de saída para cada tipo de saída (formatado vs. não formatado). Outras implementações precisariam alocar memória dinamicamente para cada chamada para conter a nova string inteira (o que é ruim no ambiente de sistemas embarcados). Minha sugestão reduz o número de saídas.O otimizador não está realmente no controle do desempenho do seu programa, você está. Use algoritmos e estruturas apropriados e perfil, perfil, perfil.
Dito isso, você não deve fazer um loop interno em uma pequena função de um arquivo em outro arquivo, pois isso impede que seja embutido.
Evite pegar o endereço de uma variável, se possível. Pedir um ponteiro não está "livre", pois significa que a variável precisa ser mantida na memória. Mesmo uma matriz pode ser mantida em registros se você evitar ponteiros - isso é essencial para vetorizar.
O que nos leva ao próximo ponto, leia o manual ^ # $ @ ! O GCC pode vetorizar código C simples se você espalhar um
__restrict__
aqui e outro__attribute__( __aligned__ )
ali. Se você deseja algo muito específico do otimizador, talvez seja necessário ser específico.fonte
A.c
entrar noB.c
.Na maioria dos processadores modernos, o maior gargalo é a memória.
Aliasing: Load-Hit-Store pode ser devastador em um loop fechado. Se você está lendo um local da memória e escrevendo em outro e sabe que eles são separados, colocar cuidadosamente uma palavra-chave alias nos parâmetros da função pode realmente ajudar o compilador a gerar um código mais rápido. No entanto, se as regiões de memória se sobrepõem e você usou 'alias', você terá uma boa sessão de depuração de comportamentos indefinidos!
Cache-miss: Não tenho certeza de como você pode ajudar o compilador, já que é principalmente algorítmico, mas há intrínsecos para pré-buscar memória.
Também não tente converter valores de ponto flutuante em int e vice-versa muito, pois eles usam registradores diferentes e a conversão de um tipo para outro significa chamar a instrução de conversão real, gravando o valor na memória e lendo-o de volta no conjunto de registradores adequado .
fonte
A grande maioria do código que as pessoas escrevem será limitado por E / S (acredito que todo o código que escrevi por dinheiro nos últimos 30 anos foi limitado), portanto, as atividades do otimizador para a maioria das pessoas serão acadêmicas.
No entanto, gostaria de lembrar às pessoas que para o código ser otimizado, você tem que dizer ao compilador para otimizá-lo - muitas pessoas (inclusive eu quando me esqueço) postam benchmarks de C ++ aqui que não fazem sentido sem o otimizador estar habilitado.
fonte
use const correção tanto quanto possível em seu código. Ele permite que o compilador otimize muito melhor.
Neste documento há muitas outras dicas de otimização: otimizações de CPP (um documento um pouco antigo)
luzes:
fonte
const
erestrict
qualificada ponteiro, no entanto, não está definida. Portanto, um compilador pode otimizar de forma diferente nesse caso.const
de umaconst
referência ouconst
ponteiro para umconst
objeto não é bem definido. modificar umconst
objeto real (ou seja, um declarado comoconst
originalmente) não é.Tente programar usando atribuição única estática, tanto quanto possível. SSA é exatamente o mesmo que você obtém na maioria das linguagens de programação funcionais, e é para isso que a maioria dos compiladores converte seu código para fazer suas otimizações, porque é mais fácil de trabalhar. Ao fazer isso, locais onde o compilador pode ficar confuso são trazidos à luz. Ele também faz com que todos os alocadores de registro, exceto os piores, funcionem tão bem quanto os melhores alocadores de registro, e permite depurar mais facilmente porque você quase nunca precisa se perguntar de onde uma variável obteve seu valor, pois havia apenas um lugar onde ela foi atribuída.
Evite variáveis globais.
Ao trabalhar com dados por referência ou ponteiro, coloque-os nas variáveis locais, faça seu trabalho e copie-os de volta. (a menos que você tenha um bom motivo para não)
Use a comparação quase gratuita com 0 que a maioria dos processadores fornece ao fazer operações matemáticas ou lógicas. Quase sempre você obtém um sinalizador para == 0 e <0, dos quais pode facilmente obter 3 condições:
quase sempre é mais barato do que testar outras constantes.
Outro truque é usar a subtração para eliminar uma comparação no teste de alcance.
Isso muitas vezes pode evitar um salto em linguagens que fazem curto-circuito em expressões booleanas e evita que o compilador tenha que tentar descobrir como lidar com o acompanhamento do resultado da primeira comparação enquanto faz a segunda e depois os combina. Pode parecer que tem o potencial de usar um registro extra, mas quase nunca o faz. Freqüentemente, você não precisa mais do foo e, se precisar, o rc ainda não é usado, então ele pode ir para lá.
Ao usar as funções de string em c (strcpy, memcpy, ...) lembre-se do que elas retornam - o destino! Freqüentemente, você pode obter um código melhor 'esquecendo' sua cópia do ponteiro para o destino e apenas recuperá-lo do retorno dessas funções.
Nunca ignore a oportunidade de retornar exatamente a mesma coisa que a última função que você chamou retornou. Compiladores não são tão bons em pegar isso:
Claro, você poderia inverter a lógica disso se e apenas tivesse um ponto de retorno.
(truques que lembrei mais tarde)
Declarar funções como estáticas quando possível é sempre uma boa ideia. Se o compilador puder provar a si mesmo que foi responsável por cada chamador de uma função específica, ele pode quebrar as convenções de chamada para essa função em nome da otimização. Os compiladores geralmente podem evitar mover parâmetros para registradores ou posições de pilha em que as funções chamadas normalmente esperam que seus parâmetros estejam (para fazer isso, é necessário desviar tanto na função chamada quanto na localização de todos os chamadores). O compilador também pode aproveitar a vantagem de saber qual memória e registros a função chamada precisará e evitar a geração de código para preservar valores de variáveis que estão em registros ou locais de memória que a função chamada não perturba. Isso funciona especialmente bem quando há poucas chamadas para uma função.
fonte
Eu escrevi um compilador C otimizado e aqui estão algumas coisas muito úteis a serem consideradas:
Torne a maioria das funções estáticas. Isso permite que a propagação da constante interprocedural e a análise de alias façam seu trabalho, caso contrário, o compilador precisa presumir que a função pode ser chamada de fora da unidade de tradução com valores completamente desconhecidos para os parâmetros. Se você olhar para as bibliotecas de código aberto bem conhecidas, todas marcam funções como estáticas, exceto aquelas que realmente precisam ser externas.
Se variáveis globais forem usadas, marque-as como estáticas e constantes, se possível. Se eles forem inicializados uma vez (somente leitura), é melhor usar uma lista de inicializadores como static const int VAL [] = {1,2,3,4}, caso contrário, o compilador pode não descobrir que as variáveis são realmente constantes inicializadas e não conseguirá substituir as cargas da variável pelas constantes.
NUNCA use um goto para dentro de um loop, o loop não será mais reconhecido pela maioria dos compiladores e nenhuma das otimizações mais importantes será aplicada.
Use parâmetros de ponteiro apenas se necessário e marque-os como restritos, se possível. Isso ajuda muito a análise de alias porque o programador garante que não há alias (a análise de alias interprocedural geralmente é muito primitiva). Objetos de estrutura muito pequenos devem ser passados por valor, não por referência.
Use arrays em vez de ponteiros sempre que possível, especialmente dentro de loops (a [i]). Um array geralmente oferece mais informações para análise de alias e, após algumas otimizações, o mesmo código será gerado de qualquer maneira (pesquise por redução de força de loop se estiver curioso). Isso também aumenta a chance de o movimento do código invariante do loop ser aplicado.
Tente içar fora das chamadas de loop para funções grandes ou funções externas que não tenham efeitos colaterais (não dependa da iteração do loop atual). Em muitos casos, pequenas funções são embutidas ou convertidas em intrínsecos que são fáceis de içar, mas funções grandes podem parecer para o compilador ter efeitos colaterais, quando na verdade não têm. Os efeitos colaterais para funções externas são completamente desconhecidos, com exceção de algumas funções da biblioteca padrão que às vezes são modeladas por alguns compiladores, tornando possível o movimento do código invariável em loop.
Ao escrever testes com várias condições, coloque o mais provável primeiro. if (a || b || c) deveria ser if (b || a || c) se b é mais provável de ser verdadeiro do que os outros. Os compiladores geralmente não sabem nada sobre os valores possíveis das condições e quais ramificações são mais usadas (eles podem ser conhecidos usando informações de perfil, mas poucos programadores as usam).
Usar um switch é mais rápido do que fazer um teste como if (a || b || ... || z). Verifique primeiro se o seu compilador faz isso automaticamente, alguns fazem e é mais legível ter o if .
fonte
No caso de sistemas embarcados e código escrito em C / C ++, tento evitar a alocação dinâmica de memória o máximo possível. O principal motivo de eu fazer isso não é necessariamente o desempenho, mas essa regra prática tem implicações no desempenho.
Algoritmos usados para gerenciar o heap são notoriamente lentos em algumas plataformas (por exemplo, vxworks). Pior ainda, o tempo que leva para retornar de uma chamada para malloc é altamente dependente do estado atual do heap. Portanto, qualquer função que chame malloc terá um impacto de desempenho que não pode ser facilmente contabilizado. Esse impacto no desempenho pode ser mínimo se o heap ainda estiver limpo, mas depois que o dispositivo for executado por um tempo, o heap pode ficar fragmentado. As chamadas vão demorar mais e você não pode calcular facilmente como o desempenho irá diminuir com o tempo. Você realmente não pode produzir uma estimativa de pior caso. O otimizador também não pode fornecer ajuda nesse caso. Para piorar as coisas, se o heap se tornar muito fragmentado, as chamadas começarão a falhar completamente. A solução é usar pools de memória (por exemplo,fatias glib ) em vez da pilha. As chamadas de alocação serão muito mais rápidas e determinísticas se você fizer isso da maneira certa.
fonte
Uma dica idiota, mas que vai economizar algumas quantidades microscópicas de velocidade e código.
Sempre passe os argumentos da função na mesma ordem.
Se você tiver f_1 (x, y, z) que chama f_2, declare f_2 como f_2 (x, y, z). Não o declare como f_2 (x, z, y).
A razão para isso é que a plataforma C / C ++ ABI (convenção de chamada também conhecida como convenção de chamada) promete passar argumentos em registradores específicos e locais de pilha. Quando os argumentos já estão nos registradores corretos, não é necessário movê-los.
Ao ler o código desmontado, vi alguns registros ridículos embaralhados porque as pessoas não seguiram essa regra.
fonte
Duas técnicas de codificação que não vi na lista acima:
Ignore o linker escrevendo o código como uma fonte única
Embora a compilação separada seja muito boa para o tempo de compilação, é muito ruim quando se fala em otimização. Basicamente, o compilador não pode otimizar além da unidade de compilação, que é o domínio reservado do vinculador.
Mas se você projetar bem seu programa, também poderá compilá-lo por meio de uma fonte comum única. Em vez de compilar unit1.ce unit2.c, vincule os dois objetos, compile all.c que simplesmente #inclui unit1.ce unit2.c. Assim, você se beneficiará de todas as otimizações do compilador.
É muito parecido com escrever cabeçalhos de programas apenas em C ++ (e ainda mais fácil de fazer em C).
Esta técnica é bastante fácil se você escrever seu programa para habilitá-lo desde o início, mas você também deve estar ciente de que ela altera parte da semântica C e você pode encontrar alguns problemas como variáveis estáticas ou colisão de macro. Para a maioria dos programas, é fácil superar os pequenos problemas que ocorrem. Também esteja ciente de que a compilação como uma fonte única é muito mais lenta e pode consumir uma grande quantidade de memória (geralmente não é um problema com sistemas modernos).
Usando essa técnica simples, aconteceu que alguns programas que escrevi dez vezes mais rápido!
Como a palavra-chave register, esse truque também pode se tornar obsoleto em breve. A otimização através do linker começa a ser suportada pelos compiladores gcc: Link time optimization .
Tarefas atômicas separadas em loops
Este é mais complicado. É sobre a interação entre o projeto do algoritmo e a maneira como o otimizador gerencia o cache e a alocação de registros. Freqüentemente, os programas precisam fazer um loop sobre alguma estrutura de dados e, para cada item, executar algumas ações. Muitas vezes, as ações realizadas podem ser divididas entre duas tarefas logicamente independentes. Se for esse o caso, você pode escrever exatamente o mesmo programa com dois loops no mesmo limite executando exatamente uma tarefa. Em alguns casos, escrever desta forma pode ser mais rápido do que o loop exclusivo (os detalhes são mais complexos, mas uma explicação pode ser que com o caso de tarefa simples todas as variáveis podem ser mantidas nos registros do processador e com o mais complexo não é possível e alguns registradores devem ser gravados na memória e lidos posteriormente e o custo é maior do que o controle de fluxo adicional).
Tenha cuidado com este (desempenho de perfil usando este truque ou não), pois como usar o registro, ele também pode fornecer desempenhos menores do que melhores.
fonte
Na verdade, eu já vi isso feito no SQLite e eles afirmam que resulta em aumentos de desempenho de ~ 5%: Coloque todo o seu código em um arquivo ou use o pré-processador para fazer o equivalente a isso. Dessa forma, o otimizador terá acesso a todo o programa e poderá fazer mais otimizações interprocedurais.
fonte
-O3
- ele explodiu 22% do tamanho original do meu programa. (Não é limitado pela CPU, então não tenho muito a dizer sobre velocidade.)A maioria dos compiladores modernos deve fazer um bom trabalho ao acelerar a recursão de cauda , porque as chamadas de função podem ser otimizadas.
Exemplo:
Claro que este exemplo não tem nenhuma verificação de limites.
Edição Tardia
Embora eu não tenha conhecimento direto do código; parece claro que os requisitos de uso de CTEs no SQL Server foram projetados especificamente para que possam ser otimizados por meio da recursão final.
fonte
Não faça o mesmo trabalho repetidamente!
Um antipadrão comum que vejo segue estas linhas:
Na verdade, o compilador precisa chamar todas essas funções o tempo todo. Supondo que você, o programador, saiba que o objeto agregado não está mudando no decorrer dessas chamadas, pelo amor de tudo que é sagrado ...
No caso do getter singleton, as chamadas podem não ser muito caras, mas certamente é um custo (normalmente, "verifique se o objeto foi criado, se não foi, crie-o e, em seguida, devolva-o). mais complicada se torna essa cadeia de getters, mais tempo perdido teremos.
fonte
Use o escopo mais local possível para todas as declarações de variáveis.
Use
const
sempre que possívelNão use o registro a menos que planeje criar um perfil com e sem ele
Os 2 primeiros, especialmente o 1, ajudam o otimizador a analisar o código. Isso o ajudará especialmente a fazer boas escolhas sobre quais variáveis manter nos registros.
Usar cegamente a palavra-chave register pode tanto ajudar quanto prejudicar sua otimização. É muito difícil saber o que importa até você olhar para a saída ou perfil da montagem.
Existem outras coisas que são importantes para obter um bom desempenho do código; projetar suas estruturas de dados para maximizar a coerência do cache, por exemplo. Mas a questão era sobre o otimizador.
fonte
Alinhe seus dados aos limites nativos / naturais.
fonte
Lembrei-me de algo que encontrei uma vez, em que o sintoma era simplesmente que estávamos ficando sem memória, mas o resultado foi um desempenho substancialmente aprimorado (bem como grandes reduções no consumo de memória).
O problema, neste caso, era que o software que estávamos usando fazia toneladas de pequenas alocações. Por exemplo, alocar quatro bytes aqui, seis bytes ali, etc. Muitos pequenos objetos, também, rodando na faixa de 8-12 bytes. O problema não era tanto que o programa precisasse de muitas pequenas coisas, mas que alocava várias pequenas coisas individualmente, o que aumentava cada alocação para (nesta plataforma em particular) 32 bytes.
Parte da solução foi montar um pool de pequenos objetos no estilo Alexandrescu, mas estendê-lo para que eu pudesse alocar matrizes de pequenos objetos, bem como itens individuais. Isso também ajudou imensamente no desempenho, pois mais itens cabem no cache ao mesmo tempo.
A outra parte da solução era substituir o uso excessivo de membros char * gerenciados manualmente por uma string SSO (otimização de string pequena). A alocação mínima sendo de 32 bytes, eu construí uma classe de string que tinha um buffer de 28 caracteres embutido atrás de um char *, então 95% de nossas strings não precisaram fazer uma alocação adicional (e então eu substituí manualmente quase todas as aparências de char * nesta biblioteca com esta nova classe, que foi divertida ou não). Isso também ajudou muito com a fragmentação da memória, o que aumentou a localidade de referência para outros objetos apontados e, da mesma forma, houve ganhos de desempenho.
fonte
Uma técnica bacana que aprendi com o comentário de @MSalters sobre esta resposta permite que os compiladores façam a elisão de cópia mesmo ao retornar objetos diferentes de acordo com alguma condição:
fonte
Se você tem pequenas funções que chama repetidamente, já tive grandes ganhos colocando-as nos cabeçalhos como "inline estático". Chamadas de função no ix86 são surpreendentemente caras.
Reimplementar funções recursivas de maneira não recursiva usando uma pilha explícita também pode ganhar muito, mas você realmente está no reino do tempo de desenvolvimento versus ganho.
fonte
Aqui está meu segundo conselho de otimização. Tal como acontece com o meu primeiro conselho, este é um propósito geral, não é específico para linguagem ou processador.
Leia o manual do compilador completamente e entenda o que ele está dizendo a você. Use o compilador ao máximo.
Eu concordo com um ou dois dos outros entrevistados que identificaram a seleção do algoritmo certo como crítica para espremer o desempenho de um programa. Além disso, a taxa de retorno (medida na melhoria da execução do código) no tempo que você investe no uso do compilador é muito maior do que a taxa de retorno no ajuste do código.
Sim, os escritores de compiladores não são de uma raça de gigantes da codificação e os compiladores contêm erros e o que deveria, de acordo com o manual e a teoria do compilador, tornar as coisas mais rápidas às vezes torna as coisas mais lentas. É por isso que você deve dar um passo de cada vez e medir o desempenho antes e depois de ajustar.
E sim, no final das contas, você pode se deparar com uma explosão combinatória de sinalizadores do compilador, então você precisa ter um script ou dois para executar o make com vários sinalizadores do compilador, enfileirar os trabalhos no grande cluster e reunir as estatísticas de tempo de execução. Se for apenas você e o Visual Studio em um PC, você perderá o interesse muito antes de tentar combinações suficientes de sinalizadores de compilador suficientes.
Saudações
Marca
Quando eu pego um pedaço de código, geralmente posso obter um fator de 1,4 - 2,0 vezes mais desempenho (ou seja, a nova versão do código é executada em 1 / 1,4 ou 1/2 do tempo da versão antiga) dentro de um um ou dois dias mexendo nos sinalizadores do compilador. Concedido, isso pode ser um comentário sobre a falta de conhecimento do compilador entre os cientistas que originaram grande parte do código em que trabalho, ao invés de um sintoma de minha excelência. Tendo definido os sinalizadores do compilador para o máximo (e raramente é apenas -O3), pode levar meses de trabalho duro para obter outro fator de 1,05 ou 1,1
fonte
Quando o DEC saiu com seus processadores alfa, havia uma recomendação para manter o número de argumentos para uma função abaixo de 7, já que o compilador sempre tentaria colocar até 6 argumentos nos registradores automaticamente.
fonte
Para desempenho, concentre-se primeiro em escrever código de manutenção - componentizado, fracamente acoplado, etc, então quando você tiver que isolar uma parte para reescrever, otimizar ou simplesmente criar um perfil, você pode fazer isso sem muito esforço.
O otimizador ajudará marginalmente o desempenho do seu programa.
fonte
Você está recebendo boas respostas aqui, mas eles presumem que seu programa está muito perto do ideal para começar, e você diz
Na minha experiência, um programa pode ser escrito corretamente, mas isso não significa que seja próximo do ideal. É preciso trabalho extra para chegar a esse ponto.
Se eu puder dar um exemplo, esta resposta mostra como um programa de aparência perfeitamente razoável foi feito 40 vezes mais rápido por macro-otimização . Grandes acelerações não podem ser feitas em todos os programas inicialmente escritos, mas em muitos (exceto para programas muito pequenos), pode, na minha experiência.
Depois que isso for feito, a micro-otimização (dos pontos críticos) pode dar uma boa recompensa.
fonte
eu uso o compilador Intel. no Windows e no Linux.
quando mais ou menos pronto, crio o perfil do código. em seguida, aguarde os pontos de acesso e tente alterar o código para permitir que o compilador faça um trabalho melhor.
se um código é computacional e contém muitos loops - o relatório de vetorização no compilador Intel é muito útil - procure por 'vec-report' na ajuda.
portanto, a ideia principal - polir o código crítico de desempenho. quanto ao resto - prioridade a ser correta e sustentável - funções curtas, código claro que poderia ser entendido 1 ano depois.
fonte
Uma otimização que usei em C ++ é a criação de um construtor que não faz nada. Deve-se chamar manualmente um init () para colocar o objeto em um estado de funcionamento.
Isso traz benefícios no caso de eu precisar de um grande vetor dessas classes.
Chamo reserve () para alocar o espaço para o vetor, mas o construtor não toca na página de memória em que o objeto está. Portanto, gastei algum espaço de endereço, mas não consumi realmente muita memória física. Eu evito as falhas de página associadas aos custos de construção associados.
Conforme eu gero objetos para preencher o vetor, eu os defino usando init (). Isso limita o total de falhas de página e evita a necessidade de redimensionar () o vetor ao preenchê-lo.
fonte
Uma coisa que fiz foi tentar manter as ações caras em lugares onde o usuário pode esperar que o programa demore um pouco. O desempenho geral está relacionado à capacidade de resposta, mas não é exatamente o mesmo e, para muitas coisas, a capacidade de resposta é a parte mais importante do desempenho.
A última vez que realmente tive que fazer melhorias no desempenho geral, fiquei atento a algoritmos subótimos e procurei locais que provavelmente apresentariam problemas de cache. Fiz o perfil e avaliei o desempenho primeiro e novamente após cada mudança. Então a empresa entrou em colapso, mas foi um trabalho interessante e instrutivo de qualquer maneira.
fonte
Há muito tempo eu suspeitava, mas nunca provei, que declarar matrizes de modo que tenham uma potência de 2, como o número de elementos, permite que o otimizador faça uma redução de força substituindo uma multiplicação por um deslocamento por um número de bits, ao olhar para cima elementos individuais.
fonte
val * 7
transformou no que seria de outra forma(val << 3) - val
.Coloque funções pequenas e / ou freqüentemente chamadas no topo do arquivo de origem. Isso torna mais fácil para o compilador encontrar oportunidades para inlining.
fonte