¿Por qué la compilación de C ++ tarda tanto?

540

Compilar un archivo C ++ lleva mucho tiempo en comparación con C # y Java. Se tarda mucho más tiempo en compilar un archivo C ++ que en ejecutar un script Python de tamaño normal. Actualmente estoy usando VC ++ pero es lo mismo con cualquier compilador. ¿Por qué es esto?

Las dos razones por las que podía pensar eran cargar archivos de encabezado y ejecutar el preprocesador, pero eso no parece que deba explicar por qué lleva tanto tiempo.

Dan Goldstein
fuente
58
VC ++ admite encabezados precompilados. Usarlos ayudará. Mucho.
Brian
1
Sí, en mi caso (en su mayoría C con algunas clases - no hay plantillas) encabezados precompilados aceleran aproximadamente 10x
Lothar
@Brian Sin embargo, nunca usaría una cabeza precompilada en una biblioteca
Cole Johnson el
13
It takes significantly longer to compile a C++ file- ¿Quieres decir 2 segundos en comparación con 1 segundo? Ciertamente, eso es el doble de largo, pero apenas significativo. ¿O quieres decir 10 minutos en comparación con 5 segundos? Por favor cuantificar.
Nick Gammon
2
Aposté por los módulos; No espero que los proyectos de C ++ se vuelvan más rápidos de construir que en otro lenguaje de programación, solo con módulos, pero puede acercarse mucho a la mayoría de los proyectos con algo de administración. Espero ver un buen administrador de paquetes con integración de artefactos después de los módulos
Abdurrahim

Respuestas:

800

Muchas rasones

Archivos de encabezado

Cada unidad de compilación requiere cientos o incluso miles de encabezados para (1) cargar y (2) compilar. Normalmente, cada uno de ellos debe volver a compilarse para cada unidad de compilación, ya que el preprocesador garantiza que el resultado de compilar un encabezado puede variar entre cada unidad de compilación. (Se puede definir una macro en una unidad de compilación que cambia el contenido del encabezado).

Esta es probablemente la razón principal, ya que requiere que se compile una gran cantidad de código para cada unidad de compilación y, además, cada encabezado debe compilarse varias veces (una vez por cada unidad de compilación que lo incluya).

Enlace

Una vez compilados, todos los archivos de objetos deben vincularse entre sí. Este es básicamente un proceso monolítico que no se puede paralelizar muy bien y tiene que procesar todo su proyecto.

Analizando

La sintaxis es extremadamente complicada de analizar, depende en gran medida del contexto y es muy difícil de eliminar. Esto lleva mucho tiempo.

Plantillas

En C #, List<T>es el único tipo que se compila, sin importar cuántas instancias de Lista tenga en su programa. En C ++, vector<int>es un tipo completamente separado de vector<float>, y cada uno tendrá que compilarse por separado.

Agregue a esto que las plantillas forman un completo "sub-idioma" completo de Turing que el compilador tiene que interpretar, y esto puede volverse ridículamente complicado. Incluso el código de metaprogramación de plantilla relativamente simple puede definir plantillas recursivas que crean docenas y docenas de instancias de plantillas. Las plantillas también pueden dar como resultado tipos extremadamente complejos, con nombres ridículamente largos, que agregan mucho trabajo adicional al vinculador. (Tiene que comparar muchos nombres de símbolos, y si estos nombres pueden convertirse en muchos miles de caracteres, eso puede llegar a ser bastante costoso).

Y, por supuesto, exacerban los problemas con los archivos de encabezado, porque las plantillas generalmente deben definirse en encabezados, lo que significa que se debe analizar y compilar mucho más código para cada unidad de compilación. En el código C simple, un encabezado generalmente solo contiene declaraciones directas, pero muy poco código real. En C ++, no es raro que casi todo el código resida en archivos de encabezado.

Mejoramiento

C ++ permite algunas optimizaciones muy dramáticas. C # o Java no permiten que las clases se eliminen por completo (tienen que estar allí para fines de reflexión), pero incluso un simple metaprograma de plantilla de C ++ puede generar fácilmente docenas o cientos de clases, todas las cuales están alineadas y eliminadas nuevamente en la optimización fase.

Además, el compilador debe optimizar completamente un programa C ++. El programa AC # puede confiar en el compilador JIT para realizar optimizaciones adicionales en el momento de la carga, C ++ no tiene esas "segundas oportunidades". Lo que genera el compilador está tan optimizado como lo va a ser.

Máquina

C ++ se compila en código máquina que puede ser algo más complicado que el uso de bytecode Java o .NET (especialmente en el caso de x86). (Esto se menciona por completo solo porque se mencionó en comentarios y demás. En la práctica, es poco probable que este paso tome más de una pequeña fracción del tiempo total de compilación).

Conclusión

La mayoría de estos factores son compartidos por el código C, que en realidad se compila de manera bastante eficiente. El paso de análisis es mucho más complicado en C ++ y puede llevar mucho más tiempo, pero el delincuente principal es probablemente las plantillas. Son útiles y hacen que C ++ sea un lenguaje mucho más poderoso, pero también tienen su costo en términos de velocidad de compilación.

jalf
fuente
38
Con respecto al punto 3: la compilación de C es notablemente más rápida que C ++. Definitivamente es la interfaz que causa la desaceleración, y no la generación de código.
Tom
72
Con respecto a las plantillas: no solo el vector <int> debe compilarse por separado del vector <double>, sino que el vector <int> se recompila en cada unidad de compilación que lo utiliza. El enlazador elimina las definiciones redundantes.
David Rodríguez - dribeas
15
dribeas: cierto, pero eso no es específico para las plantillas. Las funciones en línea o cualquier otra cosa definida en los encabezados se volverán a compilar en todas partes. Pero sí, eso es especialmente doloroso con las plantillas. :)
jalf
15
@configurator: Visual Studio y gcc permiten encabezados precompilados, lo que puede traer algunas aceleraciones serias a la compilación.
small_duck
55
No estoy seguro de si la optimización es el problema, ya que nuestras compilaciones DEBUG son en realidad más lentas que las compilaciones en modo de lanzamiento. La generación pdb también es culpable.
gast128
40

La desaceleración no es necesariamente la misma con ningún compilador.

No he usado Delphi o Kylix, pero en los días de MS-DOS, un programa Turbo Pascal se compilaba casi instantáneamente, mientras que el programa equivalente Turbo C ++ simplemente se arrastraba.

Las dos diferencias principales eran un sistema de módulos muy fuerte y una sintaxis que permitía la compilación de un solo paso.

Ciertamente es posible que la velocidad de compilación no haya sido una prioridad para los desarrolladores de compiladores de C ++, pero también hay algunas complicaciones inherentes en la sintaxis de C / C ++ que dificultan el procesamiento. (No soy un experto en C, pero Walter Bright lo es, y después de construir varios compiladores comerciales de C / C ++, creó el lenguaje D. Uno de sus cambios fue imponer una gramática libre de contexto para facilitar el análisis del lenguaje .)

Además, notará que, en general, los Makefiles están configurados para que cada archivo se compile por separado en C, por lo que si 10 archivos de origen usan el mismo archivo de inclusión, ese archivo de inclusión se procesa 10 veces.

tormenta tangente
fuente
38
Es interesante comparar a Pascal, ya que Niklaus Wirth utilizó el tiempo que le tomó al compilador compilarse a sí mismo como punto de referencia al diseñar sus lenguajes y compiladores. Hay una historia de que después de escribir cuidadosamente un módulo para una búsqueda rápida de símbolos, lo reemplazó con una búsqueda lineal simple porque el tamaño de código reducido hizo que el compilador se compilara más rápido.
Dietrich Epp
1
@DietrichEpp El empirismo vale la pena.
Tomas Zubiri
40

El análisis y la generación de código son bastante rápidos. El verdadero problema es abrir y cerrar archivos. Recuerde, incluso con incluir guardias, el compilador aún tiene abierto el archivo .H y lee cada línea (y luego la ignora).

Un amigo una vez (mientras estaba aburrido en el trabajo), tomó la solicitud de su empresa y colocó todo, todos los archivos de origen y encabezado, en un archivo grande. El tiempo de compilación se redujo de 3 horas a 7 minutos.

James Curran
fuente
14
Bueno, el acceso a archivos seguramente tiene algo que ver con esto, pero como dijo jalf, la razón principal de esto será otra cosa, a saber, el análisis repetido de muchos, muchos, muchos (¡anidados!) Archivos de encabezado que se descartan por completo en su caso.
Konrad Rudolph el
99
Es en ese momento que su amigo necesita configurar encabezados precompilados, romper dependencias entre diferentes archivos de encabezado (trate de evitar un encabezado que incluya otro, en lugar de enviar una declaración) y obtener un HDD más rápido. Aparte de eso, una métrica bastante sorprendente.
Tom Leys el
66
Si todo el archivo de encabezado (excepto posibles comentarios y líneas vacías) está dentro de los protectores de encabezado, gcc puede recordar el archivo y omitirlo si se define el símbolo correcto.
CesarB
11
El análisis es un gran problema. Para N pares de archivos de origen / encabezado de tamaño similar con interdependencias, hay O (N ^ 2) pasa a través de archivos de encabezado. Poner todo el texto en un solo archivo es reducir ese análisis duplicado.
Tom
99
Pequeña nota al margen: los protectores incluyen protección contra múltiples análisis por unidad de compilación. No en contra de múltiples análisis en general.
Marco van de Voort el
16

Otra razón es el uso del preprocesador C para localizar declaraciones. Incluso con los protectores de encabezado, .h aún debe analizarse una y otra vez, cada vez que se incluyen. Algunos compiladores admiten encabezados precompilados que pueden ayudar con esto, pero no siempre se usan.

Ver también: C ++ Respuestas frecuentes

Dave Ray
fuente
Creo que debería poner en negrita el comentario en los encabezados precompilados para señalar esta parte IMPORTANTE de su respuesta.
Kevin
66
Si todo el archivo de encabezado (excepto posibles comentarios y líneas vacías) está dentro de los protectores de encabezado, gcc puede recordar el archivo y omitirlo si se define el símbolo correcto.
CesarB
55
@CesarB: todavía tiene que procesarlo una vez por unidad de compilación (archivo .cpp).
Sam Harwell
16

C ++ se compila en código máquina. Entonces tienes el preprocesador, el compilador, el optimizador y finalmente el ensamblador, todo lo cual tiene que ejecutarse.

Java y C # se compilan en código de bytes / IL, y la máquina virtual Java / .NET Framework se ejecuta (o la compilación JIT en el código de máquina) antes de la ejecución.

Python es un lenguaje interpretado que también se compila en código de bytes.

Estoy seguro de que también hay otras razones para esto, pero en general, no tener que compilar en lenguaje máquina nativo ahorra tiempo.

Alan
fuente
15
El costo agregado por el preprocesamiento es trivial. La "otra razón" principal para una desaceleración es que la compilación se divide en tareas separadas (una por archivo de objeto), por lo que los encabezados comunes se procesan una y otra vez. Ese es el peor caso de O (N ^ 2), en comparación con la mayoría de los otros idiomas O (N) tiempo de análisis.
Tom
12
Se podría decir por la misma argumentación que los compiladores de C, Pascal, etc. son lentos, lo cual no es cierto en promedio. Tiene más que ver con la gramática de C ++ y el gran estado que debe mantener un compilador de C ++.
Sebastian Mach
2
C es lento. Sufre el mismo problema de análisis de encabezado que la solución aceptada. Por ejemplo, tome un programa simple de Windows GUI que incluya windows.h en algunas unidades de compilación y mida el rendimiento de la compilación a medida que agrega unidades de compilación (cortas).
Marco van de Voort
14

Los mayores problemas son:

1) El análisis de encabezado infinito. Ya mencionado. Las mitigaciones (como #pragma una vez) generalmente solo funcionan por unidad de compilación, no por compilación.

2) El hecho de que la cadena de herramientas a menudo se separa en múltiples archivos binarios (make, preprocesador, compilador, ensamblador, archivador, impdef, enlazador y dlltool en casos extremos) que todos tienen que reinicializar y recargar todo el estado todo el tiempo para cada invocación ( compilador, ensamblador) o cada par de archivos (archivador, enlazador y dlltool).

Vea también esta discusión en comp.compilers: http://compilers.iecc.com/comparch/article/03-11-078 especialmente esta:

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

Tenga en cuenta que John, el moderador de comp.compilers, parece estar de acuerdo, y que esto significa que también debería ser posible lograr velocidades similares para C, si uno integra la cadena de herramientas completamente e implementa encabezados precompilados. Muchos compiladores comerciales de C hacen esto hasta cierto punto.

Tenga en cuenta que el modelo Unix de factorizar todo en un binario separado es una especie de peor modelo para Windows (con su lenta creación de procesos). Es muy notable cuando se comparan los tiempos de compilación de GCC entre Windows y * nix, especialmente si el sistema make / configure también llama a algunos programas solo para obtener información.

Marco van de Voort
fuente
13

Construyendo C / C ++: lo que realmente sucede y por qué tarda tanto

Una parte relativamente grande del tiempo de desarrollo de software no se gasta en escribir, ejecutar, depurar o incluso diseñar código, sino en esperar a que termine de compilarse. Para acelerar las cosas, primero tenemos que entender lo que sucede cuando se compila el software C / C ++. Los pasos son aproximadamente los siguientes:

  • Configuración
  • Crear herramienta de inicio
  • Comprobación de dependencia
  • Compilacion
  • Enlace

Ahora veremos cada paso con más detalle enfocándonos en cómo se pueden hacer más rápido.

Configuración

Este es el primer paso al comenzar a construir. Por lo general, significa ejecutar un script de configuración o CMake, Gyp, SCons o alguna otra herramienta. Esto puede tomar desde un segundo hasta varios minutos para scripts de configuración basados ​​en Autotools muy grandes.

Este paso ocurre relativamente raramente. Solo necesita ejecutarse al cambiar configuraciones o al cambiar la configuración de compilación. A menos que cambien los sistemas de compilación, no hay mucho por hacer para acelerar este paso.

Crear herramienta de inicio

Esto es lo que sucede cuando ejecuta make o hace clic en el icono de construcción en un IDE (que generalmente es un alias para make). El binario de la herramienta de compilación se inicia y lee sus archivos de configuración, así como la configuración de compilación, que generalmente son lo mismo.

Dependiendo de la complejidad y el tamaño de la construcción, esto puede tomar desde una fracción de segundo hasta varios segundos. Por sí solo esto no sería tan malo. Desafortunadamente, la mayoría de los sistemas de compilación basados ​​en make hacen que se invoque decenas a cientos de veces por cada compilación. Por lo general, esto es causado por el uso recursivo de make (que es malo).

Cabe señalar que la razón por la que Make es tan lento no es un error de implementación. La sintaxis de Makefiles tiene algunas peculiaridades que hacen que una implementación realmente rápida sea casi imposible. Este problema es aún más notable cuando se combina con el siguiente paso.

Comprobación de dependencia

Una vez que la herramienta de compilación ha leído su configuración, tiene que determinar qué archivos han cambiado y cuáles deben recompilarse. Los archivos de configuración contienen un gráfico acíclico dirigido que describe las dependencias de compilación. Este gráfico generalmente se construye durante el paso de configuración. El tiempo de inicio de la herramienta de compilación y el escáner de dependencias se ejecutan en cada compilación. Su tiempo de ejecución combinado determina el límite inferior en el ciclo de edición-compilación-depuración. Para proyectos pequeños, este tiempo suele ser de unos segundos. Esto es tolerable Hay alternativas para hacer. El más rápido de ellos es Ninja, que fue construido por ingenieros de Google para Chromium. Si está utilizando CMake o Gyp para construir, simplemente cambie a sus backends Ninja. No tiene que cambiar nada en los archivos de compilación, solo disfrute del aumento de velocidad. Sin embargo, Ninja no está empaquetado en la mayoría de las distribuciones,

Compilacion

En este punto, finalmente invocamos el compilador. Cortando algunas esquinas, aquí están los pasos aproximados tomados.

  • La fusión incluye
  • Analizando el código
  • Generación / optimización de código

Contrariamente a la creencia popular, compilar C ++ no es realmente tan lento. El STL es lento y la mayoría de las herramientas de compilación utilizadas para compilar C ++ son lentas. Sin embargo, existen herramientas y formas más rápidas para mitigar las partes lentas del lenguaje.

Usarlos requiere un poco de grasa, pero los beneficios son innegables. Los tiempos de construcción más rápidos conducen a desarrolladores más felices, más agilidad y, finalmente, un mejor código.

Ravindra Acharya
fuente
9

Un lenguaje compilado siempre requerirá una sobrecarga inicial mayor que un lenguaje interpretado. Además, quizás no estructuraste muy bien tu código C ++. Por ejemplo:

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

Compila mucho más lento que:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}
Andy Brice
fuente
3
Especialmente cierto si BigClass incluye 5 archivos más que usa, eventualmente incluyendo todo el código en su programa.
Tom Leys el
77
Esta es quizás una razón. Pero Pascal, por ejemplo, solo toma una décima parte del tiempo de compilación que toma un programa C ++ equivalente. Esto no se debe a que la optimización de gcc: s tarde más, sino que Pascal es más fácil de analizar y no tiene que lidiar con un preprocesador. Ver también el compilador Digital Mars D.
Daniel O
2
No es el análisis más fácil, es la modularidad lo que evita reinterpretar windows.h y muchos otros encabezados para cada unidad de compilación. Sí, Pascal analiza más fácilmente (aunque los maduros, como Delphi, son más complicados nuevamente), pero eso no es lo que marca la gran diferencia.
Marco van de Voort
1
La técnica que se muestra aquí que ofrece una mejora en la velocidad de compilación se conoce como declaración directa .
DavidRR
escribir clases en un solo archivo. ¿No sería un código desordenado?
Fennekin
8

Una manera fácil de reducir el tiempo de compilación en proyectos C ++ más grandes es hacer un archivo de inclusión * .cpp que incluya todos los archivos cpp en su proyecto y compilarlo. Esto reduce el problema de explosión del encabezado a una vez. La ventaja de esto es que los errores de compilación aún harán referencia al archivo correcto.

Por ejemplo, suponga que tiene a.cpp, b.cpp y c.cpp .. cree un archivo: everything.cpp:

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

Luego compile el proyecto simplemente haciendo todo.cpp

rileyberton
fuente
3
No veo la objeción a este método. Suponiendo que genera las inclusiones desde un script o Makefile, no es un problema de mantenimiento. De hecho, acelera la compilación sin ofuscar los problemas de compilación. Se podría discutir el consumo de memoria en la compilación, pero eso rara vez es un problema en la máquina moderna. Entonces, ¿cuál es el objeto de este enfoque (aparte de la afirmación de que está mal)?
rileyberton
99
@rileyberton (ya que alguien votó por tu comentario) déjame explicarlo: no, no acelera la compilación. De hecho, se asegura de que cualquier compilación tome la mayor cantidad de tiempo al no aislar las unidades de traducción. Lo mejor de ellos es que no necesita recompilar todos los archivos .cpp si no cambiaron. (Eso sin tener en cuenta los argumentos estilísticos). La gestión adecuada de la dependencia y quizás los encabezados precompilados son mucho mejores.
Sehe
77
Lo sentimos, pero este puede ser un método muy eficiente para acelerar la compilación, porque (1) prácticamente eliminas los enlaces y (2) solo tienes que procesar los encabezados de uso común una vez. Además, funciona en la práctica , si te molestas en probarlo. Desafortunadamente, hace que las reconstrucciones incrementales sean imposibles, por lo que cada compilación es completamente nueva. Pero una reconstrucción completa con este método es mucho más rápido de lo que obtendría de otra manera
jalf
44
@BartekBanachewicz seguro, pero lo que dijiste fue que "no acelera la compilación", sin calificadores. Como dijiste, hace que cada compilación tome la cantidad máxima de tiempo (sin reconstrucciones parciales), pero al mismo tiempo, reduce drásticamente el máximo en comparación con lo que de otro modo sería. Solo digo que es un poco más matizado que "no hagas esto"
jalf
2
Diviértete con las variables y funciones estáticas. Si quiero una gran unidad de compilación, crearé un gran archivo .cpp.
gnasher729
6

Algunas razones son:

1) La gramática de C ++ es más compleja que C # o Java y lleva más tiempo analizarla.

2) (Más importante) el compilador de C ++ produce código de máquina y realiza todas las optimizaciones durante la compilación. C # y Java van solo hasta la mitad y dejan estos pasos a JIT.

Nemanja Trifunovic
fuente
5

La compensación que está obteniendo es que el programa se ejecuta un poco más rápido. Puede ser un consuelo frío para usted durante el desarrollo, pero podría ser muy importante una vez que se complete el desarrollo, y el programa solo lo estén ejecutando los usuarios.

TED
fuente
4

La mayoría de las respuestas no son claras al mencionar que C # siempre se ejecutará más lentamente debido al costo de realizar acciones que en C ++ se realizan solo una vez en el momento de la compilación, este costo de rendimiento también se ve afectado por las dependencias de tiempo de ejecución (más cosas para cargar para poder para ejecutar), sin mencionar que los programas de C # siempre tendrán una mayor huella de memoria, lo que dará como resultado que el rendimiento esté más estrechamente relacionado con la capacidad del hardware disponible. Lo mismo es cierto para otros idiomas que se interpretan o dependen de una VM.

Pánico
fuente
4

Se me ocurren dos problemas que podrían estar afectando la velocidad a la que se compilan sus programas en C ++.

POSIBLE PROBLEMA # 1 - COMPILAR EL ENCABEZADO: (Esto puede o no haber sido abordado por otra respuesta o comentario). Microsoft Visual C ++ (AKA VC ++) admite encabezados precompilados, lo cual recomiendo encarecidamente. Cuando crea un nuevo proyecto y selecciona el tipo de programa que está realizando, debe aparecer una ventana del asistente de configuración en su pantalla. Si presiona el botón "Siguiente>" en la parte inferior, la ventana lo llevará a una página que tiene varias listas de características; asegúrese de que la casilla junto a la opción "Encabezado precompilado" esté marcada. (NOTA: Esta ha sido mi experiencia con las aplicaciones de consola Win32 en C ++, pero este puede no ser el caso con todo tipo de programas en C ++).

POSIBLE PROBLEMA # 2: LA UBICACIÓN SE COMPILA CON: Este verano, tomé un curso de programación, y tuvimos que almacenar todos nuestros proyectos en unidades flash de 8GB, ya que las computadoras en el laboratorio que estábamos usando se borraban todas las noches a la medianoche, lo que habría borrado todo nuestro trabajo. Si está compilando en un dispositivo de almacenamiento externo en aras de la portabilidad / seguridad / etc., puede llevar mucho tiempotiempo (incluso con los encabezados precompilados que mencioné anteriormente) para que su programa se compile, especialmente si es un programa bastante grande. Mi consejo para usted en este caso sería crear y compilar programas en el disco duro de la computadora que está utilizando, y cada vez que quiera / necesite dejar de trabajar en su proyecto (s) por cualquier razón, transfiéralos a su externo dispositivo de almacenamiento, y luego haga clic en el icono "Quitar hardware con seguridad y expulsar medios", que debe aparecer como una pequeña unidad flash detrás de un pequeño círculo verde con una marca de verificación blanca para desconectarlo.

Espero que esto te ayude; déjame saber si lo hace! :)

cjor530
fuente