Optimização pode ocorrer em vários níveis. Normalmente os níveis mais altos têm maior impacto, e são mais difíceis de mudar mais tarde em um projeto, exigindo mudanças significativas ou uma reescrita completa se precisarem ser mudados. Assim, a otimização pode tipicamente prosseguir via refinamento de maior para menor, com ganhos iniciais sendo maiores e alcançados com menos trabalho, e ganhos posteriores sendo menores e exigindo mais trabalho. Contudo, em alguns casos, o desempenho geral depende do desempenho de partes muito baixas de um programa, e pequenas mudanças numa fase tardia ou na consideração precoce de detalhes de baixo nível podem ter um impacto maior. Tipicamente alguma consideração é dada à eficiência ao longo de um projeto – embora isto varie significativamente – mas uma grande otimização é freqüentemente considerada um refinamento a ser feito tarde, se é que alguma vez será feito. Em projetos mais longos, há normalmente ciclos de otimização, onde a melhoria de uma área revela limitações em outra, e estes são tipicamente reduzidos quando o desempenho é aceitável ou os ganhos se tornam muito pequenos ou caros.
Como o desempenho é parte da especificação de um programa – um programa que é inutilizavelmente lento não é adequado ao propósito: um jogo de vídeo com 60 Hz (frames por segundo) é aceitável, mas 6 frames por segundo é inaceitavelmente instável – o desempenho é uma consideração desde o início, para garantir que o sistema seja capaz de fornecer desempenho suficiente, e os primeiros protótipos precisam ter um desempenho mais ou menos aceitável para que haja confiança de que o sistema final (com otimização) alcançará um desempenho aceitável. Isso às vezes é omitido na crença de que a otimização sempre pode ser feita mais tarde, resultando em protótipos de sistemas que são muito lentos – muitas vezes por uma ordem de magnitude ou mais – e sistemas que no final das contas são falhas porque não conseguem alcançar seus objetivos de desempenho, como o Intel 432 (1981); ou aqueles que levam anos de trabalho para alcançar desempenho aceitável, como o Java (1995), que só alcançou desempenho aceitável com o HotSpot (1999). O grau em que o desempenho muda entre o protótipo e o sistema de produção, e como ele é favorável à otimização, pode ser uma fonte significativa de incerteza e risco.
Nível de projetoEditar
No nível mais alto, o projeto pode ser otimizado para fazer o melhor uso dos recursos disponíveis, dados os objetivos, as restrições e o uso/carga esperado. O projeto arquitetônico de um sistema afeta de forma esmagadora o seu desempenho. Por exemplo, um sistema que esteja ligado à latência da rede (onde a latência da rede é a principal restrição ao desempenho geral) seria otimizado para minimizar as viagens da rede, idealmente fazendo uma única solicitação (ou nenhuma solicitação, como em um protocolo push) ao invés de múltiplas viagens de ida e volta. A escolha do projeto depende dos objetivos: ao projetar um compilador, se compilação rápida é a prioridade chave, um compilador de uma passagem é mais rápido que um compilador multi-pass (assumindo o mesmo trabalho), mas se a velocidade do código de saída é o objetivo, um compilador multi-pass mais lento cumpre melhor o objetivo, mesmo que ele mesmo leve mais tempo. A escolha da plataforma e linguagem de programação ocorre neste nível, e mudá-las frequentemente requer uma reescrita completa, embora um sistema modular possa permitir a reescrita de apenas algum componente – por exemplo, um programa Python pode reescrever seções críticas de performance em C. Em um sistema distribuído, a escolha da arquitetura (cliente-servidor, peer-to-peer, etc.) ocorre no nível de projeto, e pode ser difícil de mudar, particularmente se todos os componentes não puderem ser substituídos em sincronia (por exemplo clientes antigos).
Algoritmos e estruturas de dadosEditar
Dando um projeto geral, uma boa escolha de algoritmos e estruturas de dados eficientes, e a implementação eficiente desses algoritmos e estruturas de dados vem a seguir. Após o projeto, a escolha dos algoritmos e estruturas de dados afeta a eficiência mais do que qualquer outro aspecto do programa. Geralmente as estruturas de dados são mais difíceis de mudar do que os algoritmos, pois uma suposição de estrutura de dados e suas suposições de desempenho são usadas ao longo do programa, embora isso possa ser minimizado pelo uso de tipos de dados abstratos nas definições de funções, e mantendo as definições concretas da estrutura de dados restritas a alguns lugares.
Para algoritmos, isso consiste principalmente em assegurar que os algoritmos sejam constantes O(1), logarítmicos O(log n), lineares O(n), ou em alguns casos log-lineares O(n log n) na entrada (tanto no espaço quanto no tempo). Algoritmos com complexidade quadrática O(n2) falham na escala, e mesmo algoritmos lineares causam problemas se chamados repetidamente, e são tipicamente substituídos por constantes ou logarítmicos se possível.
Acima da ordem assimptótica de crescimento, os fatores constantes importam: um algoritmo assimptóticamente mais lento pode ser mais rápido ou menor (porque mais simples) do que um algoritmo assimptóticamente mais rápido quando ambos são confrontados com um pequeno input, que pode ser o caso que ocorre na realidade. Muitas vezes um algoritmo híbrido irá fornecer o melhor desempenho, devido a esta mudança de compromisso com o tamanho.
Uma técnica geral para melhorar o desempenho é evitar o trabalho. Um bom exemplo é o uso de um caminho rápido para casos comuns, melhorando o desempenho ao evitar trabalho desnecessário. Por exemplo, usando um algoritmo simples de layout de texto para texto latino, apenas mudando para um algoritmo de layout complexo para scripts complexos, como o Devanagari. Outra técnica importante é o cache, particularmente a memorização, que evita computações redundantes. Devido à importância do cache, muitas vezes existem muitos níveis de cache em um sistema, o que pode causar problemas de uso de memória, e problemas de correção de caches obsoletos.
Nível de código fonteEditar
Além dos algoritmos gerais e sua implementação em uma máquina abstrata, escolhas concretas de nível de código fonte podem fazer uma diferença significativa. Por exemplo, nos primeiros compiladores em C, while(1)
foi mais lento que for(;;)
para um loop incondicional, porque while(1)
avaliou 1 e depois teve um salto condicional que testou se era verdade, enquanto for (;;)
teve um salto incondicional . Algumas otimizações (como esta) podem hoje em dia ser realizadas através da otimização de compiladores. Isto depende da linguagem do código fonte, da linguagem da máquina alvo e do compilador, e pode ser difícil de entender ou prever e mudar com o tempo; este é um lugar chave onde a compreensão dos compiladores e do código da máquina pode melhorar a performance. A optimização do código em loop e do valor de retorno são exemplos de optimizações que reduzem a necessidade de variáveis auxiliares e podem mesmo resultar numa performance mais rápida, evitando optimizações em todo o processo.
Build levelEdit
Entre o código-fonte e o nível de compilação, diretivas e flags de compilação podem ser usados para afinar opções de performance no código-fonte e compilador respectivamente, como o uso de definições de pré-processador para desabilitar recursos de software desnecessários, otimização para modelos específicos de processador ou capacidades de hardware, ou previsão de ramificações, por exemplo. Sistemas de distribuição de software baseados no código-fonte como o BSD’s Ports e o Gentoo’s Portage podem tirar vantagem desta forma de otimização.
Compile levelEdit
O uso de um compilador otimizador tende a garantir que o programa executável seja otimizado pelo menos tanto quanto o compilador pode prever.
Assembly levelEdit
No nível mais baixo, escrever código usando uma linguagem assembly, projetada para uma plataforma de hardware particular pode produzir o código mais eficiente e compacto se o programador tirar vantagem do repertório completo de instruções da máquina. Muitos sistemas operacionais usados em sistemas embarcados têm sido tradicionalmente escritos em código assembler por este motivo. Programas (além de programas muito pequenos) raramente são escritos do início ao fim em assembler devido ao tempo e custo envolvidos. A maioria é compilada a partir de uma linguagem de alto nível até a montagem e otimizada manualmente a partir daí. Quando a eficiência e tamanho são menos importantes grandes partes podem ser escritas em uma linguagem de alto nível.
Com compiladores mais modernos de otimização e a maior complexidade das CPUs recentes, é mais difícil escrever código mais eficiente do que o que o compilador gera, e poucos projetos precisam deste “último” passo de otimização.
Muito código escrito hoje em dia é destinado a rodar no maior número de máquinas possível. Como consequência, programadores e compiladores nem sempre aproveitam as instruções mais eficientes fornecidas pelas CPUs mais recentes ou as peculiaridades dos modelos mais antigos. Além disso, o código de montagem ajustado para um determinado processador sem usar tais instruções ainda pode ser subótimo em um processador diferente, esperando uma afinação diferente do código.
Tipicamente hoje, ao invés de escrever em linguagem assembly, os programadores usarão um disassembler para analisar a saída de um compilador e alterar o código fonte de alto nível para que ele possa ser compilado de forma mais eficiente, ou entender porque ele é ineficiente.
Tempo de execuçãoEditar
Compiladores just-in-time podem produzir código de máquina personalizado com base em dados de tempo de execução, ao custo de overhead de compilação. Esta técnica data dos primeiros motores de expressão regular, e tornou-se generalizada com Java HotSpot e V8 para JavaScript. Em alguns casos, a otimização adaptativa pode ser capaz de realizar a otimização do tempo de execução excedendo a capacidade dos compiladores estáticos, ajustando dinamicamente os parâmetros de acordo com a entrada real ou outros fatores.
Otimização orientada por perfil é uma técnica de otimização de compilação antecipada (AOT) baseada em perfis de tempo de execução, e é semelhante a um “caso médio” estático analógico da técnica dinâmica de otimização adaptativa.
Código auto-modificador pode se alterar em resposta a condições de tempo de execução para otimizar o código; isto era mais comum em programas de linguagem assembly.
Alguns projetos de CPU podem realizar algumas otimizações em tempo de execução. Alguns exemplos incluem Execução fora de ordem, Execução especulativa, Pipelines de instrução e Previsão de ramificações. Compiladores podem ajudar o programa a tirar vantagem desses recursos da CPU, por exemplo, através do agendamento de instruções.
Otimizações dependentes e independentes da plataformaEditar
Otimização de código também pode ser amplamente categorizada como dependente da plataforma e independente da plataforma. Enquanto estas últimas são eficazes na maioria ou em todas as plataformas, as técnicas dependentes da plataforma utilizam propriedades específicas de uma plataforma, ou dependem de parâmetros dependendo da plataforma única ou mesmo do processador único. Portanto, pode ser necessário escrever ou produzir diferentes versões do mesmo código para diferentes processadores. Por exemplo, no caso da otimização em nível de compilação, técnicas independentes da plataforma são técnicas genéricas (como o desenrolamento de loop, redução nas chamadas de funções, rotinas de eficiência de memória, redução nas condições, etc.), que impactam a maioria das arquiteturas de CPU de forma similar. Um grande exemplo de otimização independente de plataforma foi mostrado com loop interno para loop, onde foi observado que um loop com um loop interno para loop realiza mais cálculos por unidade de tempo do que um loop sem ele ou um com um loop interno para loop. Geralmente, estes servem para reduzir o comprimento total do caminho de instrução necessário para completar o programa e/ou reduzir o uso total de memória durante o processo. Por outro lado, as técnicas dependentes da plataforma envolvem programação de instruções, paralelismo em nível de instrução, paralelismo em nível de dados, técnicas de otimização de cache (ou seja, parâmetros que diferem entre várias plataformas) e a programação ótima de instruções pode ser diferente mesmo em processadores diferentes da mesma arquitetura.