Quais técnicas podem ser usadas para acelerar os tempos de compilação do C ++?
Esta questão surgiu em alguns comentários à questão Stack Overflow estilo de programação C ++ da, e estou interessado em saber quais são as idéias.
Eu já vi uma pergunta relacionada: por que a compilação C ++ demora tanto? , mas isso não fornece muitas soluções.
Respostas:
Técnicas de linguagem
Pimpl Idiom
Dê uma olhada no idioma Pimpl aqui e aqui , também conhecido como ponteiro opaco ou manipular classes. Além de acelerar a compilação, também aumenta a segurança das exceções quando combinada com uma troca sem arremesso função de . O idioma do Pimpl permite reduzir as dependências entre os cabeçalhos e reduz a quantidade de recompilação que precisa ser feita.
Declarações futuras
Sempre que possível, use declarações avançadas . Se o compilador precisar apenas saber que
SomeIdentifier
é uma estrutura, um ponteiro ou o que for, não inclua a definição inteira, forçando o compilador a fazer mais trabalho do que o necessário. Isso pode ter um efeito em cascata, tornando esse caminho mais lento do que o necessário.Os fluxos de E / S são particularmente conhecidos por diminuir a velocidade das construções. Se você precisar deles em um arquivo de cabeçalho, tente #including em
<iosfwd>
vez de<iostream>
e #include o<iostream>
cabeçalho somente no arquivo de implementação. o<iosfwd>
cabeçalho contém apenas declarações de encaminhamento. Infelizmente, os outros cabeçalhos padrão não têm um cabeçalho de declaração respectivo.Prefira passagem por referência a passagem por valor nas assinaturas de função. Isso eliminará a necessidade de # incluir as respectivas definições de tipo no arquivo de cabeçalho e você só precisará declarar o tipo adiante. Obviamente, prefira referências const a referências não const para evitar bugs obscuros, mas isso é um problema para outra pergunta.
Condições de guarda
Use condições de guarda para impedir que os arquivos de cabeçalho sejam incluídos mais de uma vez em uma única unidade de tradução.
Ao usar o pragma e o ifndef, você obtém a portabilidade da solução macro simples, bem como a otimização da velocidade de compilação que alguns compiladores podem fazer na presença do
pragma once
diretiva.Reduzir a interdependência
Quanto mais modular e menos interdependente seu design de código for, em geral, menos você precisará recompilar tudo. Você também pode acabar reduzindo a quantidade de trabalho que o compilador deve executar em qualquer bloco individual ao mesmo tempo, devido ao fato de ter menos para acompanhar.
Opções do compilador
Cabeçalhos pré-compilados
Eles são usados para compilar uma seção comum dos cabeçalhos incluídos uma vez para muitas unidades de tradução. O compilador o compila uma vez e salva seu estado interno. Esse estado pode ser carregado rapidamente para começar a compilar outro arquivo com o mesmo conjunto de cabeçalhos.
Tenha cuidado para incluir apenas itens raramente alterados nos cabeçalhos pré-compilados, ou você poderá fazer reconstruções completas com mais frequência do que o necessário. Este é um bom lugar para STL cabeçalhos e outros arquivos de inclusão da biblioteca.
O ccache é outro utilitário que tira proveito das técnicas de cache para acelerar as coisas.
Usar paralelismo
Muitos compiladores / IDEs suportam o uso de vários núcleos / CPUs para fazer a compilação simultaneamente. No GNU Make (geralmente usado com o GCC), use a
-j [N]
opção No Visual Studio, há uma opção em preferências para permitir a criação de vários projetos em paralelo. Você também pode usar a/MP
opção para paralelismo em nível de arquivo, em vez de apenas paralelismo em nível de projeto.Outros utilitários paralelos:
Use um nível mais baixo de otimização
Quanto mais o compilador tenta otimizar, mais difícil ele tem que trabalhar.
Bibliotecas compartilhadas
Mover o código modificado com menos frequência para as bibliotecas pode reduzir o tempo de compilação. Usando bibliotecas compartilhadas (
.so
ou.dll
), você também pode reduzir o tempo de vinculação.Obtenha um computador mais rápido
Mais memória RAM, discos rígidos mais rápidos (incluindo SSDs) e mais CPUs / núcleos farão a diferença na velocidade de compilação.
fonte
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: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çõesCXXFLAGS
no seu Makefile ou nas opções da linha de comando:Ou
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:
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,
e ambos
T
eU
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:
Sabemos que esta classe pode ter três instâncias possíveis:
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:
Essa técnica pode economizar seu tempo se você estiver compilando testes diferentes com um conjunto comum de instanciações.
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 incluemtuple
de STL . Você pode criar umfoo-all.cc
que se parece com: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:
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ê incluirYOUR_HEADER.hpp
em algum outro arquivo, o compilador utilizará o seu eYOUR_HEADER.hpp.gch
nãoYOUR_HEADER.hpp
na mesma pasta antes.Há dois problemas com essa técnica:
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:
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:
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:
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 classefoo3
não será visível em outras TUs (foo1
efoo4
estará visível). Se você compilar com-visibility=hidden
, somentefoo1
será visível. Mesmofoo4
estaria escondido.Você pode ler mais sobre visibilidade no wiki do GCC .
fonte
Eu recomendo estes artigos em "Jogos de dentro, design e programação independentes de jogos":
É verdade que eles são muito antigos - você terá que testar novamente tudo com as versões mais recentes (ou versões disponíveis para você), para obter resultados realistas. De qualquer forma, é uma boa fonte de idéias.
fonte
Uma técnica que funcionou muito bem para mim no passado: não compile vários arquivos de origem C ++ independentemente, mas gere um arquivo C ++ que inclua todos os outros arquivos, como este:
Obviamente, isso significa que você precisa recompilar todo o código-fonte incluído, caso alguma das fontes mude, para que a árvore de dependência piore. No entanto, compilar vários arquivos de origem como uma unidade de tradução é mais rápido (pelo menos nos meus experimentos com MSVC e GCC) e gera binários menores. Eu também suspeito que o compilador tenha mais potencial para otimizações (já que ele pode ver mais código de uma vez).
Essa técnica quebra em vários casos; por exemplo, o compilador será resgatado caso dois ou mais arquivos de origem declarem uma função global com o mesmo nome. Não consegui encontrar essa técnica descrita em nenhuma das outras respostas, por isso estou mencionando aqui.
Pelo que vale a pena, o Projeto KDE usou exatamente a mesma técnica desde 1999 para criar binários otimizados (possivelmente para uma versão). A mudança para o script de configuração da construção foi chamada
--enable-final
. Por interesse arqueológico, descobri a postagem que anunciava esse recurso: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2fonte
<core-count> + N
sublistas que são compiladas paralelamente ondeN
há algum número inteiro adequado (dependendo da memória do sistema e de que outra forma a máquina é usada).Há um livro inteiro sobre esse tópico, intitulado Design de software C ++ em larga escala (escrito por John Lakos).
Os modelos pré-datam o livro, portanto, ao conteúdo desse livro, adicione "o uso de modelos também pode tornar o compilador mais lento".
fonte
Vou apenas vincular a minha outra resposta: Como você reduz o tempo de compilação e o tempo de vinculação para projetos do Visual C ++ (C ++ nativo)? . Outro ponto que quero acrescentar, mas que costuma causar problemas é usar cabeçalhos pré-compilados. Mas, por favor, use-os apenas para peças que quase nunca mudam (como cabeçalhos de kit de ferramentas da GUI). Caso contrário, eles custarão mais tempo do que economizam no final.
Outra opção é, quando você trabalha com o GNU make, ativar a
-j<N>
opção:Eu costumo tê-lo
3
desde que eu tenho um dual core aqui. Ele executará os compiladores em paralelo para diferentes unidades de conversão, desde que não haja dependências entre eles. A vinculação não pode ser feita em paralelo, pois existe apenas um processo de vinculador que vincula todos os arquivos de objeto.Mas o vinculador em si pode ser encadeado, e é isso que o vinculador ELF faz. É um código C ++ encadeado otimizado que, diz-se, vincula os arquivos de objeto ELF uma magnitude mais rápido que o antigo (e foi realmente incluído nos binutils ).
GNU gold
ld
fonte
Aqui estão alguns:
make -j2
é um bom exemplo).-O1
que-O2
ou-O3
).fonte
-j12
ao redor-j18
foram consideravelmente mais rápidos do que-j8
, como você sugere. Eu estou querendo saber quantos núcleos que você pode ter antes de banda de memória torna-se o fator limitante ...-j
com o dobro do número de núcleos reais.Depois de aplicar todos os truques de código acima (declarações avançadas, reduzindo a inclusão de cabeçalhos ao mínimo em cabeçalhos públicos, forçando a maioria dos detalhes dentro do arquivo de implementação com o Pimpl ...) e nada mais pode ser obtido no idioma, considere seu sistema de compilação . Se você usa Linux, considere o uso de distcc (compilador distribuído) e ccache (compilador de cache).
O primeiro, distcc, executa a etapa do pré-processador localmente e envia a saída para o primeiro compilador disponível na rede. Requer as mesmas versões do compilador e da biblioteca em todos os nós configurados na rede.
O último, ccache, é um cache do compilador. Ele executa novamente o pré-processador e, em seguida, verifica com um banco de dados interno (mantido em um diretório local) se esse arquivo de pré-processador já foi compilado com os mesmos parâmetros do compilador. Caso isso aconteça, ele apenas exibe o binário e a saída da primeira execução do compilador.
Ambos podem ser usados ao mesmo tempo, para que, se o ccache não tiver uma cópia local, ele possa enviá-lo através da rede para outro nó com distcc, ou apenas injetar a solução sem processamento adicional.
fonte
Quando saí da faculdade, o primeiro código C ++ real e digno de produção que eu tinha tinha essas diretivas arcanas #ifndef ... #endif entre elas, onde os cabeçalhos eram definidos. Perguntei ao cara que estava escrevendo o código sobre essas coisas abrangentes de uma maneira muito ingênua e fui apresentado ao mundo da programação em larga escala.
Voltando ao assunto, usar diretivas para impedir definições duplicadas de cabeçalho foi a primeira coisa que aprendi quando se trata de reduzir o tempo de compilação.
fonte
Mais RAM.
Alguém falou sobre unidades de RAM em outra resposta. Eu fiz isso com um 80286 e Turbo C ++ (mostra a idade) e os resultados foram fenomenais. Como foi a perda de dados quando a máquina travou.
fonte
Use declarações avançadas sempre que puder. Se uma declaração de classe usa apenas um ponteiro ou referência a um tipo, você pode simplesmente declará-la e incluir o cabeçalho do tipo no arquivo de implementação.
Por exemplo:
Menos inclusões significa muito menos trabalho para o pré-processador, se você fizer o suficiente.
fonte
Você poderia usar o Unity Builds .
O que outras pessoas estão dizendo
fonte
Usar
na parte superior dos arquivos de cabeçalho, portanto, se eles forem incluídos mais de uma vez em uma unidade de tradução, o texto do cabeçalho será incluído e analisado apenas uma vez.
fonte
Apenas para completar: uma compilação pode ser lenta porque o sistema de compilação está sendo estúpido e também porque o compilador está demorando muito tempo para fazer seu trabalho.
Leia Recursive Make Considered Nocivo (PDF) para uma discussão sobre este tópico em ambientes Unix.
fonte
Atualize seu computador
Então você tem todas as suas outras sugestões típicas
fonte
Eu tive uma idéia sobre o uso de uma unidade de RAM . Descobriu-se que, para os meus projetos, não faz muita diferença, afinal. Mas eles ainda são bem pequenos. Tente! Eu estaria interessado em ouvir o quanto isso ajudou.
fonte
A vinculação dinâmica (.so) pode ser muito mais rápida que a vinculação estática (.a). Especialmente quando você tem uma unidade de rede lenta. Isso ocorre porque você tem todo o código do arquivo .a que precisa ser processado e gravado. Além disso, um arquivo executável muito maior precisa ser gravado no disco.
fonte
Não sobre o tempo de compilação, mas sobre o tempo de compilação:
Use o ccache se precisar reconstruir os mesmos arquivos ao trabalhar em seus buildfiles
Use ninja-build em vez de make. Atualmente, estou compilando um projeto com ~ 100 arquivos de origem e tudo é armazenado em cache pelo ccache. faça necessidades 5 minutos, ninja menos que 1.
Você pode gerar seus arquivos ninja do cmake com
-GNinja
.fonte
Onde você está gastando seu tempo? Você está vinculado à CPU? Memória ligada? Disco ligado? Você pode usar mais núcleos? Mais RAM? Você precisa de RAID? Deseja simplesmente melhorar a eficiência do seu sistema atual?
Em gcc / g ++, você olhou para o ccache ? Pode ser útil se você estiver fazendo
make clean; make
muito.fonte
Discos rígidos mais rápidos.
Os compiladores gravam muitos arquivos (e possivelmente enormes) no disco. Trabalhe com SSD em vez do disco rígido típico e os tempos de compilação são muito menores.
fonte
No Linux (e talvez em alguns outros * NIXes), você pode realmente acelerar a compilação NÃO OLHANDO na saída e mudando para outra TTY.
Aqui está o experimento: printf desacelera meu programa
fonte
Os compartilhamentos de rede diminuirão drasticamente sua compilação, pois a latência de busca é alta. Para algo como o Boost, fez uma enorme diferença para mim, mesmo que nossa unidade de compartilhamento de rede seja bastante rápida. O tempo para compilar um programa Boost de brinquedo passou de 1 minuto para 1 segundo quando eu mudei de um compartilhamento de rede para um SSD local.
fonte
Se você possui um processador multicore, o Visual Studio (2005 e posterior) e o GCC suportam compilações com vários processadores. É algo para ativar se você tiver o hardware, com certeza.
fonte
Embora não seja uma "técnica", não consegui descobrir como os projetos do Win32 com muitos arquivos de origem foram compilados mais rapidamente do que o meu projeto vazio "Hello World". Assim, espero que isso ajude alguém como ele me ajudou.
No Visual Studio, uma opção para aumentar o tempo de compilação é a vinculação incremental ( / INCREMENTAL ). É incompatível com a Geração de código em tempo de link ( / LTCG ), portanto, lembre-se de desabilitar o link incremental ao criar versões.
fonte
/INCREMENTAL
no modo de depuração únicaA partir do Visual Studio 2017, você tem a capacidade de ter algumas métricas de compilador sobre o que leva tempo.
Inclua esses parâmetros em C / C ++ -> Linha de Comandos (Opções Adicionais) na janela de propriedades do projeto:
/Bt+ /d2cgsummary /d1reportTime
Você pode ter mais informações neste post .
fonte
O uso de links dinâmicos em vez de estáticos torna o compilador mais rápido possível.
Se você usar t Cmake, ative a propriedade:
Versão de compilação, o uso de links estáticos pode otimizar mais.
fonte