Por que a compilação C ++ demora tanto?


540

A compilação de um arquivo C ++ leva muito tempo quando comparada com C # e Java. Leva muito mais tempo para compilar um arquivo C ++ do que para executar um script Python de tamanho normal. Atualmente, estou usando o VC ++, mas é o mesmo com qualquer compilador. Por que é isso?

As duas razões pelas quais eu conseguia pensar estavam carregando arquivos de cabeçalho e executando o pré-processador, mas isso não parece explicar o porquê de demorar tanto.


58
O VC ++ suporta cabeçalhos pré-compilados. Usá-los ajudará. Muito.
27711 Brian

1
Sim, no meu caso (principalmente C com algumas aulas - há modelos) cabeçalhos pré-compilados acelerar cerca de 10x
Lothar

@ Brian eu nunca usaria um pré cabeça compilado em uma biblioteca embora
Cole Johnson

13
It takes significantly longer to compile a C++ file- você quer dizer 2 segundos em comparação com 1 segundo? Certamente isso é o dobro do tempo, mas dificilmente significativo. Ou você quer dizer 10 minutos em comparação com 5 segundos? Quantifique.
Nick Gammon

2
Eu apostei em módulos; Eu não espero que os projetos C ++ se tornem mais rápidos de construir do que em outra linguagem de programação, apenas com os módulos, mas ele pode ficar muito próximo da maioria dos projetos com algum gerenciamento. Espero ver um gerente bom pacote com a integração artifactory após os módulos
Abdurrahim

Respostas:


800

Vários motivos

Arquivos de cabeçalho

Cada unidade de compilação exige que centenas ou mesmo milhares de cabeçalhos sejam (1) carregados e (2) compilados. Normalmente, cada um deles deve ser recompilado para cada unidade de compilação, porque o pré-processador garante que o resultado da compilação de um cabeçalho possa variar entre cada unidade de compilação. (Uma macro pode ser definida em uma unidade de compilação que altera o conteúdo do cabeçalho).

Esse é provavelmente o principal motivo, pois exige que grandes quantidades de código sejam compiladas para cada unidade de compilação e, além disso, todo cabeçalho deve ser compilado várias vezes (uma vez para cada unidade de compilação que o inclui).

Linking

Uma vez compilados, todos os arquivos de objetos precisam ser vinculados. Este é basicamente um processo monolítico que não pode muito bem ser paralelo e precisa processar todo o seu projeto.

Análise

A sintaxe é extremamente complicada de analisar, depende muito do contexto e é muito difícil de desambiguar. Isso leva muito tempo.

Modelos

Em C #, List<T>é o único tipo que é compilado, não importa quantas instanciações da Lista você tenha no seu programa. Em C ++, vector<int>é um tipo completamente separado de vector<float>, e cada um terá que ser compilado separadamente.

Acrescente a isso que os modelos compõem uma "sub-linguagem" completa de Turing que o compilador precisa interpretar, e isso pode se tornar ridiculamente complicado. Mesmo o código de metaprogramação de modelos relativamente simples pode definir modelos recursivos que criam dezenas e dezenas de instanciações de modelos. Os modelos também podem resultar em tipos extremamente complexos, com nomes ridiculamente longos, adicionando muito trabalho extra ao vinculador. (Ele precisa comparar muitos nomes de símbolos e, se esses nomes puderem crescer em milhares de caracteres, isso poderá se tornar bastante caro).

E, é claro, eles exacerbam os problemas com os arquivos de cabeçalho, porque os modelos geralmente precisam ser definidos nos cabeçalhos, o que significa que muito mais código deve ser analisado e compilado para cada unidade de compilação. No código C simples, um cabeçalho normalmente contém apenas declarações de encaminhamento, mas muito pouco código real. No C ++, não é incomum que quase todo o código resida nos arquivos de cabeçalho.

Otimização

O C ++ permite algumas otimizações muito drásticas. C # ou Java não permitem que as classes sejam completamente eliminadas (elas precisam estar lá para fins de reflexão), mas mesmo um simples metaprograma de modelo C ++ pode gerar facilmente dezenas ou centenas de classes, todas elas alinhadas e eliminadas novamente na otimização Estágio.

Além disso, um programa C ++ deve ser totalmente otimizado pelo compilador. O programa AC # pode confiar no compilador JIT para executar otimizações adicionais no momento do carregamento, o C ++ não obtém nenhuma "segunda chance". O que o compilador gera é o mais otimizado possível.

Máquina

O C ++ é compilado no código da máquina, o que pode ser um pouco mais complicado do que o uso do Java ou .NET do bytecode (especialmente no caso do x86). (Isso é mencionado por completo apenas porque foi mencionado em comentários e outros. Na prática, é improvável que essa etapa leve mais do que uma fração minúscula do tempo total de compilação).

Conclusão

A maioria desses fatores é compartilhada pelo código C, que na verdade é compilado com bastante eficiência. A etapa de análise é muito mais complicada em C ++ e pode levar muito mais tempo, mas o principal agressor é provavelmente os modelos. Eles são úteis e tornam o C ++ uma linguagem muito mais poderosa, mas também causam prejuízos em termos de velocidade de compilação.


38
Em relação ao ponto 3: a compilação C é notavelmente mais rápida que o C ++. Definitivamente, é o frontend que causa a desaceleração, e não a geração do código.
7242 Tom

72
Em relação aos modelos: não apenas o vetor <int> deve ser compilado separadamente do vetor <double>, mas o vetor <int> é recompilado em cada unidade de compilação que o utiliza. Definições redundantes são eliminadas pelo vinculador.
David Rodríguez - dribeas 31/12/08

15
dribeas: Verdade, mas isso não é específico para modelos. As funções embutidas ou qualquer outra coisa definida nos cabeçalhos serão recompiladas em todos os lugares em que estiverem incluídas. Mas sim, isso é especialmente doloroso com os modelos. :)
jalf 31/12/08

15
@ configurador: O Visual Studio e o gcc permitem cabeçalhos pré-compilados, o que pode trazer algumas sérias acelerações para a compilação.
2119 small_duck

5
Não tenho certeza se a otimização é o problema, pois nossas compilações DEBUG são realmente mais lentas que as compiladas no modo de lançamento. A geração de pdb também é culpada.
gast128

40

A desaceleração não é necessariamente a mesma com qualquer compilador.

Eu não usei o Delphi ou o Kylix, mas nos dias de MS-DOS, um programa Turbo Pascal seria compilado quase instantaneamente, enquanto o programa Turbo C ++ equivalente era rastreado.

As duas principais diferenças eram um sistema de módulos muito forte e uma sintaxe que permitia a compilação de passagem única.

Certamente é possível que a velocidade de compilação simplesmente não tenha sido uma prioridade para os desenvolvedores de compiladores C ++, mas também existem algumas complicações inerentes à sintaxe do C / C ++ que dificultam o processamento. (Não sou especialista em C, mas Walter Bright é, e depois de criar vários compiladores comerciais de C / C ++, ele criou a linguagem D. Uma de suas mudanças foi impor uma gramática livre de contexto para facilitar a análise da linguagem .)

Além disso, você notará que geralmente os Makefiles são configurados para que todos os arquivos sejam compilados separadamente em C; portanto, se 10 arquivos de origem usarem o mesmo arquivo de inclusão, esse arquivo de inclusão será processado 10 vezes.


38
É interessante comparar Pascal, já que Niklaus Wirth usou o tempo que levou para o compilador se compilar como referência ao projetar seus idiomas e compiladores. Há uma história de que, depois de escrever cuidadosamente um módulo para pesquisa rápida de símbolos, ele o substituiu por uma pesquisa linear simples, porque o tamanho reduzido do código fez o compilador se compilar mais rapidamente.
Dietrich Epp

1
@DietrichEpp O empirismo compensa.
Tomas Zubiri

40

A análise e a geração de código são realmente bastante rápidas. O verdadeiro problema é abrir e fechar arquivos. Lembre-se, mesmo com os protetores de inclusão, o compilador ainda abriu o arquivo .H e leu cada linha (e depois o ignore).

Um amigo, uma vez (enquanto estava entediado no trabalho), pegou o aplicativo da empresa e colocou tudo - todos os arquivos de origem e cabeçalho - em um grande arquivo. O tempo de compilação caiu de 3 horas para 7 minutos.


14
Bem, o acesso a arquivos certamente tem uma mão nisso, mas, como disse jalf, a principal razão para isso será outra coisa, a saber, a análise repetida de muitos, muitos, muitos arquivos de cabeçalho (aninhados!) Que desaparecem completamente no seu caso.
21430 Konrad Rudolph

9
É nesse ponto que seu amigo precisa configurar cabeçalhos pré-compilados, quebrar dependências entre arquivos de cabeçalho diferentes (tente evitar um cabeçalho incluindo outro, em vez disso, declare para a frente) e obtenha um disco rígido mais rápido. Além disso, uma métrica bastante surpreendente.
Tom Leys

6
Se todo o arquivo de cabeçalho (exceto possíveis comentários e linhas vazias) estiver dentro dos protetores de cabeçalho, o gcc poderá se lembrar do arquivo e pular se o símbolo correto estiver definido.
CesarB

11
A análise é um grande negócio. Para N pares de arquivos de origem / cabeçalho de tamanho semelhante com interdependências, há O (N ^ 2) passa pelos arquivos de cabeçalho. Colocar todo o texto em um único arquivo está reduzindo a análise duplicada.
Tom

9
Nota lateral pequena: Os protetores de inclusão protegem contra várias análises por unidade de compilação. Não contra várias análises em geral.
Marco van de Voort

16

Outro motivo é o uso do pré-processador C para localizar declarações. Mesmo com os protetores de cabeçalho, .h ainda precisa ser analisado repetidamente, toda vez que for incluído. Alguns compiladores oferecem suporte a cabeçalhos pré-compilados que podem ajudar com isso, mas nem sempre são usados.

Veja também: Respostas frequentes sobre C ++


Eu acho que você deve colocar o comentário em negrito nos cabeçalhos pré-compilados para apontar essa parte IMPORTANTE da sua resposta.
25408 Kevin

6
Se todo o arquivo de cabeçalho (exceto possíveis comentários e linhas vazias) estiver dentro dos protetores de cabeçalho, o gcc poderá se lembrar do arquivo e pular se o símbolo correto estiver definido.
CesarB

5
@ CesarB: Ele ainda precisa processá-lo completamente uma vez por unidade de compilação (arquivo .cpp).
Sam Harwell 25/03

16

C ++ é compilado no código da máquina. Então você tem o pré-processador, o compilador, o otimizador e, finalmente, o montador, os quais precisam ser executados.

Java e C # são compilados em código de bytes / IL e a máquina virtual Java / .NET Framework é executada (ou compilada JIT em código de máquina) antes da execução.

Python é uma linguagem interpretada que também é compilada em código de bytes.

Tenho certeza de que existem outros motivos para isso também, mas, em geral, não ter que compilar no idioma nativo da máquina economiza tempo.


15
O custo adicionado pelo pré-processamento é trivial. A principal "outra razão" para uma desaceleração é que a compilação é dividida em tarefas separadas (uma por arquivo de objeto), para que os cabeçalhos comuns sejam processados ​​repetidamente. Esse é o pior caso de O (N ^ 2), versus o tempo de análise de O (N) da maioria dos outros idiomas.
Tom

12
Você pode dizer pela mesma argumentação que os compiladores C, Pascal etc. são lentos, o que não é verdade em média. Tem mais a ver com a gramática do C ++ e o enorme estado que um compilador do C ++ deve manter.
Sebastian Mach

2
C é lento. Ele sofre do mesmo problema de análise de cabeçalho da solução aceita. Por exemplo, pegue um programa simples da GUI do Windows que inclua o windows.h em algumas unidades de compilação e meça o desempenho da compilação ao adicionar (curtas) unidades de compilação.
Marco van de Voort

14

Os maiores problemas são:

1) O cabeçalho infinito reanalisando. Já mencionado. As mitigações (como #pragma uma vez) geralmente funcionam apenas por unidade de compilação, não por compilação.

2) O fato de a cadeia de ferramentas ser frequentemente separada em vários binários (marca, pré-processador, compilador, montador, arquivador, impdef, linker e dlltool em casos extremos) que todos precisam reinicializar e recarregar todo o estado o tempo todo para cada chamada ( compilador, montador) ou todos os arquivos (arquivador, vinculador e dlltool).

Veja também esta discussão em comp.compilers: http://compilers.iecc.com/comparch/article/03-11-078, especialmente esta:

http://compilers.iecc.com/comparch/article/02-07-128

Observe que John, o moderador dos comp.compilers parece concordar, e que isso significa que também deve ser possível obter velocidades semelhantes para C, se alguém integrar a cadeia de ferramentas completamente e implementar cabeçalhos pré-compilados. Muitos compiladores C comerciais fazem isso em algum grau.

Observe que o modelo Unix de fatorar tudo em um binário separado é um tipo de modelo de pior caso para o Windows (com sua lenta criação de processos). É muito perceptível ao comparar os tempos de criação do GCC entre o Windows e o * nix, especialmente se o sistema make / configure também chamar alguns programas apenas para obter informações.


13

Construindo C / C ++: o que realmente acontece e por que demora tanto

Uma parte relativamente grande do tempo de desenvolvimento de software não é gasta na gravação, execução, depuração ou até no design de código, mas na espera pela conclusão da compilação. Para agilizar as coisas, primeiro precisamos entender o que está acontecendo quando o software C / C ++ é compilado. As etapas são aproximadamente as seguintes:

  • Configuração
  • Arranque da ferramenta de construção
  • Verificação de dependência
  • Compilação
  • Linking

Agora, analisaremos cada etapa com mais detalhes, focando em como eles podem ser feitos mais rapidamente.

Configuração

Este é o primeiro passo ao iniciar a construção. Geralmente significa executar um script de configuração ou CMake, Gyp, SCons ou alguma outra ferramenta. Isso pode levar de um segundo a vários minutos para scripts de configuração muito grandes baseados em Autotools.

Este passo acontece relativamente raramente. Ele só precisa ser executado ao alterar configurações ou alterar a configuração de compilação. Com exceção da mudança dos sistemas de construção, não há muito a ser feito para acelerar esse passo.

Arranque da ferramenta de construção

É o que acontece quando você executa o make ou clica no ícone de construção em um IDE (que geralmente é um alias para o make). O binário da ferramenta de construção é iniciado e lê seus arquivos de configuração, bem como a configuração de construção, que geralmente são a mesma coisa.

Dependendo da complexidade e tamanho da compilação, isso pode levar de uma fração de segundo a vários segundos. Por si só, isso não seria tão ruim. Infelizmente, a maioria dos sistemas de build baseados em make faz com que o invocador seja invocado dezenas de centenas de vezes para cada build único. Geralmente, isso é causado pelo uso recursivo de make (o que é ruim).

Deve-se notar que o motivo pelo qual o Make é tão lento não é um bug de implementação. A sintaxe do Makefiles possui algumas peculiaridades que tornam uma implementação realmente rápida quase impossível. Esse problema é ainda mais perceptível quando combinado com a próxima etapa.

Verificação de dependência

Depois que a ferramenta de construção lê sua configuração, ela precisa determinar quais arquivos foram alterados e quais precisam ser recompilados. Os arquivos de configuração contêm um gráfico acíclico direcionado que descreve as dependências de construção. Esse gráfico geralmente é construído durante a etapa de configuração. O tempo de inicialização da ferramenta de compilação e o scanner de dependência são executados em cada compilação. Seu tempo de execução combinado determina o limite inferior no ciclo de edição-compilação-depuração. Para pequenos projetos, esse tempo geralmente leva alguns segundos. Isso é tolerável. Existem alternativas para o Make. O mais rápido deles é o Ninja, construído pelos engenheiros do Google para o Chromium. Se você estiver usando o CMake ou o Gyp para construir, mude para os back-ends Ninja deles. Você não precisa alterar nada nos arquivos de compilação, apenas aproveite o aumento de velocidade. O Ninja não é fornecido na maioria das distribuições, no entanto,

Compilação

Neste ponto, finalmente chamamos o compilador. Cortando alguns cantos, aqui estão os passos aproximados dados.

  • A fusão inclui
  • Analisando o código
  • Geração / otimização de código

Ao contrário da crença popular, compilar C ++ não é tão lento assim. O STL é lento e a maioria das ferramentas de compilação usadas para compilar C ++ é lenta. No entanto, existem ferramentas e maneiras mais rápidas de mitigar as partes lentas do idioma.

Usá-los requer um pouco de graxa nos cotovelos, mas os benefícios são inegáveis. Tempos de construção mais rápidos levam a desenvolvedores mais felizes, mais agilidade e, eventualmente, melhor código.


9

Uma linguagem compilada sempre exigirá uma sobrecarga inicial maior do que uma linguagem interpretada. Além disso, talvez você não tenha estruturado muito bem o seu código C ++. Por exemplo:

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

Compila muito mais lentamente que:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

3
Especialmente se o BigClass incluir mais 5 arquivos que ele usa, eventualmente incluindo todo o código do seu programa.
Tom Leys

7
Essa talvez seja uma razão. Mas Pascal, por exemplo, leva apenas um décimo do tempo de compilação que um programa C ++ equivalente leva. Isso não ocorre porque a otimização do gcc: s leva mais tempo, mas o Pascal é mais fácil de analisar e não precisa lidar com um pré-processador. Veja também o compilador Digital Mars D.
27409 Daniel O

2
Não é a análise mais fácil, é a modularidade que evita reinterpretar windows.he inúmeros outros cabeçalhos para cada unidade de compilação. Sim, Pascal analisa mais facilmente (embora os maduros, como Delphi, sejam mais complicados novamente), mas não é isso que faz a grande diferença.
Marco van de Voort 29/11

1
A técnica mostrada aqui, que oferece uma melhoria na velocidade de compilação, é conhecida como declaração direta .
DavidRR

escrevendo aulas em apenas um arquivo. não seria código confuso?
21415 Fennekin

8

Uma maneira fácil de reduzir o tempo de compilação em projetos C ++ maiores é criar um arquivo de inclusão * .cpp que inclua todos os arquivos cpp no ​​seu projeto e compilá-lo. Isso reduz o problema de explosão do cabeçalho para uma vez. A vantagem disso é que os erros de compilação ainda farão referência ao arquivo correto.

Por exemplo, suponha que você tenha a.cpp, b.cpp e c.cpp. Crie um arquivo: everything.cpp:

#include "a.cpp"
#include "b.cpp"
#include "c.cpp"

Em seguida, compile o projeto criando apenas everything.cpp


3
Não vejo a objeção a esse método. Supondo que você gere as inclusões a partir de um script ou Makefile, não é um problema de manutenção. De fato, acelera a compilação sem ofuscar problemas de compilação. Você poderia discutir o consumo de memória na compilação, mas isso raramente é um problema nas máquinas modernas. Então, qual é o objetivo dessa abordagem (além da afirmação de que ela está errada)?
precisa saber é o seguinte

9
@rileyberton (já que alguém votou positivamente no seu comentário), deixe-me explicitar: não, não acelera a compilação. Na verdade, ele garante que qualquer compilação leva a quantidade máxima de tempo por não isolar unidades de tradução. O melhor de tudo é que você não precisa recompilar todos os .cpp-s se eles não mudarem. (Isso desconsidera os argumentos estilísticos). O gerenciamento adequado de dependências e talvez os cabeçalhos pré-compilados são muito melhores.
sehe

7
Desculpe, mas este pode ser um método muito eficiente para acelerar a compilação, porque você (1) praticamente elimina a vinculação e (2) precisa processar apenas os cabeçalhos mais usados ​​uma vez. Além disso, funciona na prática , se você tentar. Infelizmente, isso impossibilita reconstruções incrementais, portanto, toda construção é completamente do zero. Mas uma reconstrução completa com este método é muito mais rápido do que você deseja obter de outra forma
jalf

4
@BartekBanachewicz com certeza, mas o que você disse foi que "não acelera a compilação", sem qualificadores. Como você disse, cada compilação leva a quantidade máxima de tempo (sem reconstruções parciais), mas, ao mesmo tempo, reduz drasticamente o máximo em comparação com o que seria. Eu só estou dizendo que é um pouco mais sutil do que "não faça isso"
jalf

2
Divirta-se com variáveis ​​e funções estáticas. Se eu quiser uma grande unidade de compilação, criarei um grande arquivo .cpp.
gnasher729

6

Alguns motivos são:

1) A gramática C ++ é mais complexa que C # ou Java e leva mais tempo para analisar.

2) (Mais importante) O compilador C ++ produz código de máquina e faz todas as otimizações durante a compilação. C # e Java vão apenas pela metade e deixam essas etapas para o JIT.


5

A desvantagem que você está recebendo é que o programa é executado um pouquinho mais rápido. Isso pode ser um conforto frio para você durante o desenvolvimento, mas pode ter grande importância quando o desenvolvimento estiver completo e o programa estiver sendo executado pelos usuários.


4

A maioria das respostas não é clara ao mencionar que o C # sempre será mais lento devido ao custo de executar ações que no C ++ são executadas apenas uma vez no tempo de compilação, esse custo de desempenho também é impactado devido às dependências do tempo de execução (mais coisas a serem carregadas para poder para executar), sem mencionar que os programas C # sempre terão maior espaço de memória, todos resultando em desempenho mais estreitamente relacionado à capacidade do hardware disponível. O mesmo vale para outros idiomas que são interpretados ou dependem de uma VM.


4

Existem dois problemas que podem estar afetando a velocidade com que seus programas em C ++ estão sendo compilados.

POSSÍVEL PROBLEMA # 1 - COMPILAR O CABEÇALHO: (Isso já pode ou não ter sido abordado por outra resposta ou comentário.) O Microsoft Visual C ++ (AKA VC ++) oferece suporte a cabeçalhos pré-compilados, que eu recomendo. Quando você cria um novo projeto e seleciona o tipo de programa que está criando, uma janela do assistente de instalação deve aparecer na tela. Se você pressionar o botão "Próximo>" na parte inferior, a janela o levará a uma página que possui várias listas de recursos; verifique se a caixa ao lado da opção "Cabeçalho pré-compilado" está marcada. (Observação: essa tem sido minha experiência com aplicativos de console do Win32 em C ++, mas pode não ser o caso de todos os tipos de programas em C ++.)

POSSÍVEL PROBLEMA # 2 - O LOCAL QUE ESTÁ ACOMPANHADO: Neste verão, fiz um curso de programação e tivemos que armazenar todos os nossos projetos em pen drives de 8 GB, pois os computadores do laboratório que estávamos usando eram limpos todas as noites à meia-noite, o que apagaria todo o nosso trabalho. Se você estiver compilando em um dispositivo de armazenamento externo para portabilidade / segurança / etc., Pode demorar muitotempo (mesmo com os cabeçalhos pré-compilados que eu mencionei acima) para o seu programa compilar, especialmente se for um programa bastante grande. Meu conselho para você, nesse caso, seria criar e compilar programas no disco rígido do computador que você está usando e sempre que quiser / precisar parar de trabalhar no (s) seu (s) projeto (s) por qualquer motivo, transfira-o para o seu computador externo. dispositivo de armazenamento e clique no ícone “Remover hardware com segurança e ejetar mídia”, que deve aparecer como uma pequena unidade flash atrás de um pequeno círculo verde com uma marca de seleção branca, para desconectá-lo.

Espero que isso ajude você; deixe-me saber se faz! :)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.