Melhores práticas de otimização
Este documento descreve algumas melhores práticas para otimizar programas C++ no Visual Studio.
Opções de compilador e vinculador
Otimização guiada por perfil
O Visual Studio dá suporte à PGO (otimização guiada por perfil). Essa otimização usa dados de perfil de execuções de treinamento de uma versão instrumentada de um aplicativo para impulsionar a otimização posterior do aplicativo. Usar a PGO pode ser demorado, portanto, pode não ser usada por todos os desenvolvedores, mas é recomendável usar a PGO para a compilação final de lançamento de um produto. Para obter mais informações, confira Otimizações guiadas por perfil.
Além disso, a Otimização de Programas Inteiros (também conhecida como Geração de Código de Tempo de Link) e as otimizações e as otimizações /O1
e /O2
foram aprimoradas. Em geral, um aplicativo compilado com uma dessas opções será mais rápido do que o mesmo aplicativo compilado com um compilador anterior.
Para obter mais informações, consulte /GL
(Otimização de Programas Inteiros) e /O1
, /O2
(Minimizar Tamanho, Maximizar Velocidade).
Qual o nível de otimização a ser usado
Se possível, os builds de versão final devem ser compilados com otimizações guiadas por perfil. Se não for possível criar com a PGO, seja devido à infraestrutura insuficiente para executar os builds instrumentados ou não ter acesso a cenários, sugerimos a criação com a Otimização de Programas Inteiros.
A opção /Gy
também é muito útil. Ele gera um COMDAT separado para cada função, dando ao vinculador mais flexibilidade quando se trata de remover COMDATs não referenciados e dobramento COMDAT. A única desvantagem de usar /Gy
é que ele pode causar problemas na depuração. Portanto, geralmente é recomendável usá-lo. Para obter mais informações, consulte /Gy
(Habilitar vinculação em nível de função).
Para vincular em ambientes de 64 bits, é recomendável usar a opção /OPT:REF,ICF
de vinculador e, em ambientes de 32 bits, /OPT:REF
é o recomendado. Para obter mais informações, consulte /OPT (Otimizações).
Também é altamente recomendável gerar símbolos de depuração, mesmo com builds de versão otimizados. Ele não afeta o código gerado e torna muito mais fácil depurar seu aplicativo, se necessário.
Opções de ponto flutuante
A opção /Op
do compilador foi removida e as quatro opções do compilador a seguir que tratam otimizações de ponto flutuante foram adicionadas:
Opção | Descrição |
---|---|
/fp:precise |
Essa é a recomendação padrão e deve ser usada na maioria dos casos. |
/fp:fast |
Recomendado se o desempenho for de extrema importância, por exemplo, nos jogos. Isso resultará no desempenho mais rápido. |
/fp:strict |
Recomendado se forem desejadas exceções precisas de ponto flutuante e comportamento IEEE. Isso resultará no desempenho mais lento. |
/fp:except[-] |
Pode ser usado em conjunto com /fp:strict ou /fp:precise , mas não com /fp:fast . |
Para obter mais informações, confira /fp
(Especificar comportamento de ponto flutuante).
Declspecs de otimização
Nesta seção, examinaremos dois declspecs que podem ser usados em programas para ajudar no desempenho: __declspec(restrict)
e __declspec(noalias)
.
O declspec restrict
só pode ser aplicado a declarações de função que retornam um ponteiro, como __declspec(restrict) void *malloc(size_t size);
O declspec restrict
é usado em funções que retornam ponteiros sem alias. Essa palavra-chave é usada para a implementação da Biblioteca C-Runtime de malloc
, pois ela nunca retornará um valor de ponteiro que já esteja em uso no programa atual (a menos que você esteja fazendo algo ilegal, como usar a memória depois dela ter sido liberada).
O declspec restrict
fornece ao compilador mais informações para executar otimizações do compilador. Uma das coisas mais difíceis para um compilador determinar é quais ponteiros são alias de outros ponteiros e usar essas informações ajuda muito o compilador.
Vale ressaltar que essa é uma promessa para o compilador, não algo que o compilador verificará. Se o programa usar o declspec restrict
inadequadamente, seu programa poderá ter um comportamento incorreto.
Para obter mais informações, consulte restrict
.
O declspec noalias
também é aplicado somente a funções e indica que a função é uma função semi-pura. Uma função semi-pura é aquela que faz referência ou modifica apenas locais, argumentos e indireções de primeiro nível de argumentos. Esse declspec é uma promessa para o compilador e, se a função fizer referência a indireções globais ou de segundo nível de argumentos de ponteiro, o compilador poderá gerar código que interrompe o aplicativo.
Para obter mais informações, consulte noalias
.
Pragmas de otimização
Há também vários pragmas úteis para ajudar a otimizar o código. O primeiro que discutiremos é #pragma optimize
:
#pragma optimize("{opt-list}", on | off)
Esse pragma permite definir um determinado nível de otimização em uma base função por função. É ideal para as raras ocasiões em que o aplicativo falha quando uma determinada função é compilada com otimização. Use-o para desativar otimizações para uma única função:
#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)
Para obter mais informações, consulte optimize
.
Embutimento (inlining) é uma das otimizações mais importantes que o compilador executa e aqui falamos sobre alguns dos pragmas que ajudam a modificar esse comportamento.
#pragma inline_recursion
é útil para especificar se você deseja ou não que o aplicativo seja capaz de fazer uma chamada recursiva embutida. Por padrão, ele permanece desativado. Para recursão superficial de pequenas funções, você pode ativar isso. Para obter mais informações, consulte inline_recursion
.
Outro pragma útil para limitar a profundidade do embutimento é #pragma inline_depth
. Normalmente, isso é útil em situações em que você está tentando limitar o tamanho de um programa ou função. Para obter mais informações, consulte inline_depth
.
__restrict
e __assume
Há algumas palavras-chave no Visual Studio que podem ajudar no desempenho: __restrict e __assume.
Primeiro, deve-se notar que __restrict
e __declspec(restrict)
são duas coisas diferentes. Embora estejam de certa forma relacionados, têm semântica diferente. __restrict
é um qualificador de tipo, como const
ou volatile
, mas exclusivamente para tipos de ponteiro.
Um ponteiro que é modificado com __restrict
é conhecido como um ponteiro __restrict. Um ponteiro __restrict é um ponteiro que só pode ser acessado por meio do ponteiro __restrict. Em outras palavras, outro ponteiro não pode ser usado para acessar os dados apontados pelo ponteiro __restrict.
__restrict
pode ser uma ferramenta poderosa para o otimizador do Microsoft C++, mas use-o com muito cuidado. Se usado incorretamente, o otimizador poderá executar uma otimização que interromperia seu aplicativo.
Com __assume
, um desenvolvedor pode dizer ao compilador para fazer suposições sobre o valor de alguma variável.
Por exemplo, __assume(a < 5);
informa ao otimizador que, nessa linha de código, a variável a
é menor que 5. Novamente, essa é uma promessa para o compilador. Se a
, na verdade, for 6 neste ponto do programa, o comportamento do programa após a otimização do compilador poderá não ser o esperado. __assume
é mais útil antes de alternar instruções e/ou expressões condicionais.
Há algumas limitações para o uso de __assume
: Primeiro, como __restrict
é apenas uma sugestão, portanto, o compilador é livre para ignorá-lo. Além disso, __assume
atualmente funciona apenas com desigualdades variáveis contra constantes. Ele não propaga desigualdades simbólicas, por exemplo, pressupõe(a < b).
Suporte intrínseco
Intrínsecos são chamadas de função em que o compilador tem conhecimento intrínseco sobre a chamada e, em vez de chamar uma função em uma biblioteca, ele emite código para essa função. O arquivo de cabeçalho <intrin.h> contém todos os intrínsecos disponíveis para cada uma das plataformas de hardware com suporte.
Os intrínsecos dão ao programador a capacidade de entrar profundamente no código sem precisar usar o assembly. Há vários benefícios no uso de intrínsecos:
Seu código é mais portátil. Vários intrínsecos estão disponíveis em diversas arquiteturas de CPU.
Seu código é mais fácil de ler, já que o código ainda está escrito em C/C++.
Seu código obtém o benefício das otimizações do compilador. À medida que o compilador melhora, a geração de código para os intrínsecos é aperfeiçoada.
Para obter mais informações, consulte Intrínsecos do compilador.
Exceções
Há um impacto de desempenho associado ao uso de exceções. Algumas restrições são introduzidas ao usar blocos try que inibem o compilador de executar determinadas otimizações. Em plataformas x86, há degradação de desempenho adicional de blocos try devido a informações de estado adicionais que devem ser geradas durante a execução do código. Nas plataformas de 64 bits, os blocos try não degradam tanto o desempenho, mas uma vez lançada uma exceção, o processo de localizar o manipulador e desenrolar a pilha pode ser caro.
Portanto, é recomendável evitar a introdução de blocos try/catch em códigos que realmente não precisem deles. Caso precise usar exceções, se possível use exceções síncronas. Para obter mais informações, consulte Tratamento de Exceção Estruturada (C/C++).
Por fim, gere exceções somente para casos excepcionais. O uso de exceções para o fluxo de controle geral provavelmente fará com que o desempenho piore.