Compartilhar via


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.

Confira também