Eu trabalho no projeto STAPL, que é uma biblioteca C ++ com muitos modelos. De vez em quando, temos que revisitar todas as técnicas para reduzir o tempo de compilação. Aqui, resumi as técnicas que usamos. Algumas dessas técnicas já estão listadas acima:
Localizando as seções que consomem mais tempo
Embora não haja correlação comprovada entre a duração dos símbolos e o tempo de compilação, observamos que tamanhos médios menores de símbolos podem melhorar o tempo de compilação em todos os compiladores. Então, seu primeiro objetivo é encontrar os maiores símbolos em seu código.
Método 1 - Classificar símbolos com base no tamanho
Você pode usar o nm
comando para listar os símbolos com base em seus tamanhos:
nm --print-size --size-sort --radix=d YOUR_BINARY
Neste comando, --radix=d
permite ver os tamanhos em números decimais (o padrão é hexadecimal). Agora, observando o símbolo maior, identifique se você pode quebrar a classe correspondente e tente reprojetá-la, fatorando as partes não modeladas em uma classe base ou dividindo a classe em várias classes.
Método 2 - Classificar símbolos com base no comprimento
Você pode executar o nm
comando regular e direcioná-lo ao seu script favorito ( AWK , Python etc.) para classificar os símbolos com base no comprimento . Com base em nossa experiência, esse método identifica o maior problema para tornar os candidatos melhores que o método 1.
Método 3 - Usar Templight
"O Templight é uma ferramenta baseada em Clang para analisar o tempo e o consumo de memória das instanciações de modelos e executar sessões de depuração interativas para obter introspecção no processo de instanciação de modelos".
Você pode instalar o Templight consultando o LLVM e o Clang ( instruções ) e aplicando o patch do Templight. A configuração padrão para LLVM e Clang está na depuração e asserções, e elas podem afetar significativamente o tempo de compilação. Parece que o Templight precisa de ambos, então você precisa usar as configurações padrão. O processo de instalação do LLVM e do Clang deve demorar cerca de uma hora.
Depois de aplicar o patch, você pode usar templight++
localizado na pasta de construção especificada na instalação para compilar seu código.
Verifique se templight++
está no seu PATH. Agora, para compilar, adicione as seguintes opções CXXFLAGS
no seu Makefile ou nas opções da linha de comando:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Ou
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Após a compilação, você terá um .trace.memory.pbf e .trace.pbf gerados na mesma pasta. Para visualizar esses rastreamentos, você pode usar as Ferramentas Templight que podem convertê-las para outros formatos. Siga estas instruções para instalar o templight-convert. Geralmente usamos a saída do callgrind. Você também pode usar a saída do GraphViz se o seu projeto for pequeno:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
O arquivo de callgrind gerado pode ser aberto usando o kcachegrind, no qual é possível rastrear a instanciação que consome mais tempo / memória.
Reduzindo o número de instanciações de modelo
Embora não exista uma solução exata para reduzir o número de instanciações de modelos, existem algumas diretrizes que podem ajudar:
Refatorar classes com mais de um argumento de modelo
Por exemplo, se você tem uma classe,
template <typename T, typename U>
struct foo { };
e ambos T
e U
pode ter 10 opções diferentes, você tem aumentado as possíveis instâncias de modelos desta classe a 100. Uma maneira de resolver este é abstrair a parte comum do código para uma classe diferente. O outro método é usar a inversão de herança (revertendo a hierarquia de classes), mas certifique-se de que seus objetivos de design não sejam comprometidos antes de usar esta técnica.
Refatorar código não modelo para unidades de tradução individuais
Usando esta técnica, você pode compilar a seção comum uma vez e vinculá-la às suas outras TUs (unidades de tradução) posteriormente.
Use instanciações de modelo externo (desde C ++ 11)
Se você conhece todas as instâncias possíveis de uma classe, pode usar esta técnica para compilar todos os casos em uma unidade de tradução diferente.
Por exemplo, em:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
Sabemos que esta classe pode ter três instâncias possíveis:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
Coloque o acima em uma unidade de tradução e use a palavra-chave extern no seu arquivo de cabeçalho, abaixo da definição da classe:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
Essa técnica pode economizar seu tempo se você estiver compilando testes diferentes com um conjunto comum de instanciações.
NOTA: MPICH2 ignora a instanciação explícita neste momento e sempre compila as classes instanciadas em todas as unidades de compilação.
Use construções de unidade
A idéia por trás da criação da unidade é incluir todos os arquivos .cc que você usa em um arquivo e compilar esse arquivo apenas uma vez. Usando esse método, você pode evitar o restabelecimento de seções comuns de arquivos diferentes e, se o seu projeto incluir muitos arquivos comuns, você provavelmente salvará também os acessos ao disco.
Como exemplo, vamos supor que você tem três arquivos foo1.cc
, foo2.cc
, foo3.cc
e todos eles incluem tuple
de STL . Você pode criar um foo-all.cc
que se parece com:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Você compila esse arquivo apenas uma vez e potencialmente reduz as instâncias comuns entre os três arquivos. Geralmente, é difícil prever se a melhoria pode ser significativa ou não. Mas um fato evidente é que você perderia o paralelismo em suas compilações (não é mais possível compilar os três arquivos ao mesmo tempo).
Além disso, se algum desses arquivos consumir muita memória, você poderá ficar sem memória antes que a compilação termine. Em alguns compiladores, como o GCC , isso pode causar ICE (erro interno do compilador) por falta de memória. Portanto, não use essa técnica, a menos que conheça todos os prós e contras.
Cabeçalhos pré-compilados
Cabeçalhos pré-compilados (PCHs) podem economizar muito tempo na compilação, compilando os arquivos de cabeçalho em uma representação intermediária reconhecível por um compilador. Para gerar arquivos de cabeçalho pré-compilados, você só precisa compilar seu arquivo de cabeçalho com o comando de compilação regular. Por exemplo, no GCC:
$ g++ YOUR_HEADER.hpp
Isso irá gerar uma YOUR_HEADER.hpp.gch file
( .gch
é a extensão para arquivos PCH no GCC) na mesma pasta. Isso significa que, se você incluir YOUR_HEADER.hpp
em algum outro arquivo, o compilador utilizará o seu e YOUR_HEADER.hpp.gch
não YOUR_HEADER.hpp
na mesma pasta antes.
Há dois problemas com essa técnica:
- Você precisa garantir que os arquivos de cabeçalho pré-compilados estejam estáveis e não sejam alterados ( você sempre pode alterar seu makefile )
- Você pode incluir apenas um PCH por unidade de compilação (na maioria dos compiladores). Isso significa que, se você tiver mais de um arquivo de cabeçalho a ser pré-compilado, precisará incluí-los em um arquivo (por exemplo,
all-my-headers.hpp
). Mas isso significa que você deve incluir o novo arquivo em todos os lugares. Felizmente, o GCC tem uma solução para esse problema. Use -include
e forneça o novo arquivo de cabeçalho. Você pode vírgula separar arquivos diferentes usando essa técnica.
Por exemplo:
g++ foo.cc -include all-my-headers.hpp
Use namespaces não nomeados ou anônimos
Os namespaces sem nome (também conhecidos como namespaces anônimos) podem reduzir significativamente os tamanhos binários gerados. Os espaços para nome sem nome usam ligação interna, o que significa que os símbolos gerados nesses espaços para nome não serão visíveis para outras TU (unidades de tradução ou compilação). Os compiladores geralmente geram nomes exclusivos para espaços de nome sem nome. Isso significa que se você tiver um arquivo foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
E você inclui esse arquivo em duas TUs (dois arquivos .cc e os compila separadamente). As duas instâncias de modelo foo não serão as mesmas. Isso viola a regra de definição única (ODR). Pelo mesmo motivo, o uso de espaços para nome não nomeados é desencorajado nos arquivos de cabeçalho. Sinta-se à vontade para usá-los em seus .cc
arquivos para evitar que símbolos apareçam em seus arquivos binários. Em alguns casos, alterar todos os detalhes internos de um .cc
arquivo mostrou uma redução de 10% nos tamanhos binários gerados.
Alterando as opções de visibilidade
Nos compiladores mais novos, você pode selecionar seus símbolos para serem visíveis ou invisíveis nos DSOs (Dynamic Shared Objects). Idealmente, alterar a visibilidade pode melhorar o desempenho do compilador, otimizações de tempo de link (LTOs) e tamanhos binários gerados. Se você olhar para os arquivos de cabeçalho STL no GCC, poderá ver que ele é amplamente usado. Para habilitar as opções de visibilidade, você precisa alterar seu código por função, por classe, por variável e, mais importante, por compilador.
Com a ajuda da visibilidade, você pode ocultar os símbolos que os considera privados dos objetos compartilhados gerados. No GCC, você pode controlar a visibilidade dos símbolos passando padrão ou oculto para a -visibility
opção do seu compilador. Em certo sentido, isso é semelhante ao namespace sem nome, mas de uma maneira mais elaborada e intrusiva.
Se você desejar especificar as visibilidades por caso, precisará adicionar os seguintes atributos às suas funções, variáveis e classes:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
A visibilidade padrão no GCC é padrão (pública), o que significa que se você compilar acima como um -shared
método de biblioteca compartilhada ( ), foo2
e a classe foo3
não será visível em outras TUs ( foo1
e foo4
estará visível). Se você compilar com -visibility=hidden
, somente foo1
será visível. Mesmo foo4
estaria escondido.
Você pode ler mais sobre visibilidade no wiki do GCC .