Partilhar via


Melhores práticas de otimização

Este documento descreve algumas práticas recomendadas para otimizar programas C++ no Visual Studio.

Opções do compilador e do vinculador

Otimização guiada por perfil

O Visual Studio oferece suporte à otimização guiada por perfil (PGO). 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 PGO pode ser demorado, então pode não ser algo que todo desenvolvedor usa, mas recomendamos usar PGO para a compilação final de um produto. Para obter mais informações, consulte Profile-Guided Otimizações.

Além disso, a Otimização de Todo o Programa (também conhecida como Geração de Código em Tempo de Ligação) e as otimizações /O1 e /O2 foram melhoradas. 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 todo o programa) e /O1, /O2 (Minimizar tamanho, maximizar velocidade).

Qual nível de otimização usar

Se possível, as compilações da versão final devem ser compiladas com otimizações guiadas por perfil. Se não for possível construir com PGO, seja devido à infraestrutura insuficiente para executar as compilações instrumentadas ou não ter acesso a cenários, então sugerimos construir com Otimização de Programa Completo.

O /Gy interruptor 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 dobragem COMDAT. A única desvantagem de usar /Gy é que pode causar problemas ao depurar. Portanto, geralmente recomenda-se usá-lo. Para obter mais informações, consulte /Gy (Ativar vinculação Function-Level).

Para vincular em ambientes de 64 bits, recomenda-se usar a /OPT:REF,ICF opção de vinculador e, em ambientes de 32 bits, /OPT:REF é recomendado. Para obter mais informações, consulte /OPT (Otimizações).

Também é altamente recomendável gerar símbolos de depuração, mesmo com compilações de versão otimizadas. Isso não afeta o código gerado e torna muito mais fácil depurar seu aplicativo, se necessário.

Comutadores de ponto flutuante

A /Op opção do compilador foi removida e as quatro opções do compilador a seguir que lidam com otimizações de ponto flutuante foram adicionadas:

Opção Descrição
/fp:precise Esta é 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, em 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 /fp:fast.

Para obter mais informações, consulte /fp (Especificar Floating-Point comportamento).

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 restrict declspec só pode ser aplicado a declarações de função que retornam um ponteiro, como __declspec(restrict) void *malloc(size_t size);

O restrict declspec é usado em funções que retornam ponteiros sem alias. Essa palavra-chave é usada para a implementação do C-Runtime Library, uma vez que nunca retornará um valor de malloc ponteiro que já esteja em uso no programa atual (a menos que você esteja fazendo algo ilegal, como usar memória depois que ela for liberada).

O restrict declspec fornece ao compilador mais informações para executar otimizações do compilador. Uma das coisas mais difíceis para um compilador determinar é quais ponteiros se sobrepõem a outros ponteiros, e usar essas informações ajuda muito o compilador.

Vale ressaltar que isso é uma promessa para o compilador, não algo que o compilador irá verificar. Se o seu programa usa esse restrict declspec inapropriadamente, seu programa pode ter um comportamento incorreto.

Para obter mais informações, consulte restrict.

O noalias declspec também é aplicado apenas a funções, e indica que a função é uma função semi-pura. Uma função semipura é aquela que faz referência ou modifica apenas locais, argumentos e indireções de primeiro nível dos argumentos. Este declspec é uma promessa para o compilador, e se a função faz referência a globais ou indirections de segundo nível de argumentos de ponteiro, então o compilador pode gerar código que quebra 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. A primeira que discutiremos é #pragma optimize:

#pragma optimize("{opt-list}", on | off)

Este pragma permite que você defina um determinado nível de otimização em uma base função por função. Isso é ideal para aquelas raras ocasiões em que seu aplicativo falha quando uma determinada função é compilada com otimização. Você pode usar isso para desativar otimizações para uma única função:

#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)

Para obter mais informações, consulte optimize.

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 inserir uma chamada recursiva. Por padrão, ele está 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 inlining é #pragma inline_depth. Isso geralmente é ú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.

Em primeiro lugar, deve-se notar que __restrict e __declspec(restrict) são duas coisas diferentes. Embora estejam um pouco relacionados, a sua semântica é diferente. __restrict é um qualificador de tipo, como const ou volatile, mas exclusivamente para tipos de ponteiro.

Um ponteiro modificado com __restrict é chamado de ponteiro __restrict. Um ponteiro __restrict é um ponteiro que só pode ser acessado através 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 Microsoft C++, mas use-o com muito cuidado. Se usado incorretamente, o otimizador pode executar uma otimização que quebraria seu aplicativo.

Com __assume, um desenvolvedor pode dizer ao compilador para fazer suposições sobre o valor de uma variável.

Por exemplo __assume(a < 5); , diz ao otimizador que nessa linha de código a variável a é menor que 5. Mais uma vez, esta é uma promessa para o compilador. Se a é realmente 6 neste ponto do programa, então o comportamento do programa após o compilador ter otimizado pode não ser o que você esperaria. __assume é mais útil antes de instruções de seleção (switch) e/ou expressões condicionais.

Existem algumas limitações para __assume. Primeiro, como __restrict, é apenas uma sugestão, então o compilador é livre para ignorá-lo. Além disso, __assume atualmente trabalha apenas com desigualdades variáveis contra constantes. Não propaga desigualdades simbólicas, por exemplo, assume(a < b).

Suporte intrínseco

Chamadas intrínsecas são aquelas em que o compilador possui um conhecimento específico sobre elas e, em vez de chamar uma função de uma biblioteca, gera diretamente o código correspondente a 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 suportadas.

Intrínsecos dão ao programador a capacidade de ir fundo no código sem ter que usar assembly. Existem vários benefícios em usar intrinsics:

  • Seu código é mais portátil. Vários dos intrínsecos estão disponíveis em várias arquiteturas de CPU.

  • Seu código é mais fácil de ler, uma vez 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 melhora.

Para obter mais informações, consulte Compiler Intrinsics.

Exceções

Há um impacto no 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 adicional do desempenho dos 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 significativamente o desempenho, mas uma vez que uma exceção é gerada, o processo de encontrar o manipulador e desenrolar a pilha pode ser dispendioso.

Portanto, recomenda-se evitar a introdução de blocos try/catch no código que realmente não precisa dele. Se você precisar usar exceções, use exceções síncronas, se possível. Para obter mais informações, consulte Tratamento de exceções estruturadas (C/C++).

Por fim, lance exceções apenas para casos excecionais. O uso de exceções para o fluxo de controle geral provavelmente prejudicará o desempenho.

Ver também