¿Qué técnicas se pueden usar para acelerar los tiempos de compilación de C ++?
Esta pregunta surgió en algunos comentarios al estilo de programación C ++ de Stack Overflow question , y estoy interesado en saber qué ideas hay.
He visto una pregunta relacionada: ¿Por qué la compilación de C ++ tarda tanto? , pero eso no proporciona muchas soluciones.
Respuestas:
Tecnicas de lenguaje
Pimpl Idiom
Echa un vistazo a la idioma Pimpl aquí y aquí , también conocido como puntero opaco o clases de identificador. No solo acelera la compilación, sino que también aumenta la seguridad de las excepciones cuando se combina con unafunción de intercambio sin lanzamiento . El idioma de Pimpl le permite reducir las dependencias entre los encabezados y reduce la cantidad de compilación que debe hacerse.
Declaraciones Reenviadas
Siempre que sea posible, use declaraciones a futuro . Si el compilador solo necesita saber que
SomeIdentifier
es una estructura o un puntero o lo que sea, no incluya la definición completa, forzando al compilador a hacer más trabajo del que necesita. Esto puede tener un efecto en cascada, lo que hace que sea más lento de lo necesario.Los flujos de E / S son particularmente conocidos por ralentizar las compilaciones. Si los necesita en un archivo de encabezado, intente #incluir en
<iosfwd>
lugar de #incluir<iostream>
el<iostream>
encabezado solo en el archivo de implementación. El<iosfwd>
encabezado contiene solo declaraciones hacia adelante. Lamentablemente, los otros encabezados estándar no tienen un encabezado de declaraciones respectivo.Prefiera pasar por referencia a pasar por valor en las firmas de funciones. Esto eliminará la necesidad de #incluir las definiciones de tipo respectivas en el archivo de encabezado y solo necesitará declarar el tipo hacia adelante. Por supuesto, prefiera las referencias constantes a las referencias no constantes para evitar errores oscuros, pero este es un problema para otra pregunta.
Condiciones de guardia
Use las condiciones de protección para evitar que los archivos de encabezado se incluyan más de una vez en una sola unidad de traducción.
Al usar tanto el pragma como el ifndef, obtienes la portabilidad de la solución macro simple, así como la optimización de la velocidad de compilación que algunos compiladores pueden hacer en presencia de la
pragma once
directiva.Reduce la interdependencia
Cuanto más modular y menos interdependiente sea el diseño de su código en general, con menos frecuencia tendrá que volver a compilar todo. También puede terminar reduciendo la cantidad de trabajo que el compilador tiene que hacer en cualquier bloque individual al mismo tiempo, en virtud del hecho de que tiene menos para realizar un seguimiento.
Opciones del compilador
Encabezados precompilados
Estos se utilizan para compilar una sección común de encabezados incluidos una vez para muchas unidades de traducción. El compilador lo compila una vez y guarda su estado interno. Ese estado se puede cargar rápidamente para comenzar a compilar otro archivo con ese mismo conjunto de encabezados.
Tenga cuidado de que solo incluya cosas raramente cambiadas en los encabezados precompilados, o podría terminar haciendo reconstrucciones completas con más frecuencia de la necesaria. Este es un buen lugar para los encabezados STL y otras bibliotecas que incluyen archivos.
ccache es otra utilidad que aprovecha las técnicas de almacenamiento en caché para acelerar las cosas.
Usar paralelismo
Muchos compiladores / IDE admiten el uso de múltiples núcleos / CPU para realizar la compilación simultáneamente. En GNU Make (generalmente usado con GCC), use la
-j [N]
opción. En Visual Studio, hay una opción debajo de las preferencias para permitirle construir múltiples proyectos en paralelo. También puede utilizar la/MP
opción de paralelismo a nivel de archivo, en lugar de solo paralelismo a nivel de proyecto.Otras utilidades paralelas:
Use un nivel de optimización más bajo
Cuanto más intente optimizar el compilador, más difícil será trabajar.
Bibliotecas Compartidas
Mover el código modificado con menos frecuencia a las bibliotecas puede reducir el tiempo de compilación. Al usar bibliotecas compartidas (
.so
o.dll
), también puede reducir el tiempo de enlace.Obtenga una computadora más rápida
Más RAM, discos duros más rápidos (incluidas las SSD) y más CPU / núcleos marcarán la diferencia en la velocidad de compilación.
fuente
Trabajo en el proyecto STAPL, que es una biblioteca de C ++ con muchas plantillas. De vez en cuando, tenemos que volver a visitar todas las técnicas para reducir el tiempo de compilación. Aquí, he resumido las técnicas que usamos. Algunas de estas técnicas ya se enumeran arriba:
Encontrar las secciones que requieren más tiempo
Aunque no existe una correlación probada entre las longitudes de los símbolos y el tiempo de compilación, hemos observado que los tamaños promedio de símbolos más pequeños pueden mejorar el tiempo de compilación en todos los compiladores. Entonces, su primer objetivo es encontrar los símbolos más grandes en su código.
Método 1: ordena los símbolos según el tamaño
Puede usar el
nm
comando para enumerar los símbolos en función de sus tamaños:En este comando, le
--radix=d
permite ver los tamaños en números decimales (el valor predeterminado es hexadecimal). Ahora, mirando el símbolo más grande, identifique si puede romper la clase correspondiente e intente rediseñarla factorizando las partes sin plantilla en una clase base, o dividiendo la clase en varias clases.Método 2: ordena los símbolos según la longitud
Puede ejecutar el
nm
comando regular y canalizarlo a su script favorito ( AWK , Python , etc.) para ordenar los símbolos en función de su longitud . Según nuestra experiencia, este método identifica los mayores problemas para hacer que los candidatos sean mejores que el método 1.Método 3 - Use Templight
" Templight es un Clang herramienta basada en para perfilar el consumo de tiempo y memoria de las instancias de plantillas y para realizar sesiones de depuración interactivas para ganar introspección en el proceso de creación de instancias de plantillas".
Puede instalar Templight consultando LLVM y Clang ( instrucciones ) y aplicando el parche Templight en él. La configuración predeterminada para LLVM y Clang está en depuración y afirmaciones, y esto puede afectar significativamente el tiempo de compilación. Parece que Templight necesita ambos, por lo que debe usar la configuración predeterminada. El proceso de instalación de LLVM y Clang debería llevar aproximadamente una hora.
Después de aplicar el parche, puede usarlo
templight++
ubicado en la carpeta de compilación que especificó durante la instalación para compilar su código.Asegúrate de que
templight++
esté en tu RUTA. Ahora para compilar agregue los siguientes modificadores a suCXXFLAGS
en su Makefile o a sus opciones de línea de comando:O
Una vez realizada la compilación, tendrá un .trace.memory.pbf y .trace.pbf generados en la misma carpeta. Para visualizar estos rastros, puede usar las Herramientas de Templight que pueden convertirlos a otros formatos. Siga estas instrucciones para instalar templight-convert. Usualmente usamos la salida callgrind. También puede usar la salida GraphViz si su proyecto es pequeño:
El archivo callgrind generado se puede abrir usando kcachegrind en el que puede rastrear la instanciación que consume más tiempo / memoria.
Reducir el número de instancias de plantilla
Aunque no hay una solución exacta para reducir el número de instancias de plantillas, existen algunas pautas que pueden ayudar:
Clases de refactorización con más de un argumento de plantilla
Por ejemplo, si tienes una clase,
y los dos
T
yU
puede tener 10 opciones diferentes, que han aumentado las posibles instancias de la plantilla de esta clase a 100. Una forma de resolver este es abstraer la parte común del código para una clase diferente. El otro método es utilizar la inversión de herencia (revertir la jerarquía de clases), pero asegúrese de que sus objetivos de diseño no se vean comprometidos antes de utilizar esta técnica.Refactorice el código sin plantilla para unidades de traducción individuales
Con esta técnica, puede compilar la sección común una vez y vincularla con sus otras TU (unidades de traducción) más adelante.
Use instancias de plantilla externas (desde C ++ 11)
Si conoce todas las posibles instancias de una clase, puede usar esta técnica para compilar todos los casos en una unidad de traducción diferente.
Por ejemplo, en:
Sabemos que esta clase puede tener tres posibles instancias:
Ponga lo anterior en una unidad de traducción y use la palabra clave externa en su archivo de encabezado, debajo de la definición de clase:
Esta técnica puede ahorrarle tiempo si está compilando diferentes pruebas con un conjunto común de instancias.
Usa construcciones de unidad
La idea detrás de las compilaciones de la unidad es incluir todos los archivos .cc que usa en un archivo y compilar ese archivo solo una vez. Con este método, puede evitar la reinstalación de secciones comunes de diferentes archivos y si su proyecto incluye muchos archivos comunes, probablemente también ahorraría en accesos a disco.
A modo de ejemplo, supongamos que tiene tres archivos
foo1.cc
,foo2.cc
,foo3.cc
y todos ellos incluyentuple
desde STL . Puede crear unofoo-all.cc
que se vea así:Compila este archivo solo una vez y potencialmente reduce las instancias comunes entre los tres archivos. En general, es difícil predecir si la mejora puede ser significativa o no. Pero un hecho evidente es que perderías paralelismo en sus compilaciones (ya no puede compilar los tres archivos al mismo tiempo).
Además, si alguno de estos archivos ocupa mucha memoria, es posible que se quede sin memoria antes de que termine la compilación. En algunos compiladores, como GCC , esto podría ICE (Error interno del compilador) su compilador por falta de memoria. Así que no use esta técnica a menos que conozca todos los pros y los contras.
Encabezados precompilados
Los encabezados precompilados (PCH) pueden ahorrarle mucho tiempo en la compilación compilando sus archivos de encabezado en una representación intermedia reconocible por un compilador. Para generar archivos de encabezado precompilados, solo necesita compilar su archivo de encabezado con su comando de compilación habitual. Por ejemplo, en GCC:
Esto generará un
YOUR_HEADER.hpp.gch file
(.gch
es la extensión para archivos PCH en GCC) en la misma carpeta. Esto significa que si incluyeYOUR_HEADER.hpp
en algún otro archivo, el compilador usará su enYOUR_HEADER.hpp.gch
lugar deYOUR_HEADER.hpp
en la misma carpeta antes.Hay dos problemas con esta técnica:
all-my-headers.hpp
). Pero eso significa que debe incluir el nuevo archivo en todos los lugares. Afortunadamente, GCC tiene una solución para este problema. Use-include
y dele el nuevo archivo de encabezado. Puede separar por coma diferentes archivos con esta técnica.Por ejemplo:
Use espacios de nombres anónimos o sin nombre
Los espacios de nombres sin nombre (también conocidos como espacios de nombres anónimos) pueden reducir significativamente los tamaños binarios generados. Los espacios de nombres sin nombre utilizan enlaces internos, lo que significa que los símbolos generados en esos espacios de nombres no serán visibles para otras TU (unidades de traducción o compilación). Los compiladores generalmente generan nombres únicos para espacios de nombres sin nombre. Esto significa que si tiene un archivo foo.hpp:
Y resulta que incluye este archivo en dos TU (dos archivos .cc y los compila por separado). Las dos instancias de plantilla foo no serán las mismas. Esto viola la Regla de una definición (ODR). Por la misma razón, se desaconseja el uso de espacios de nombres sin nombre en los archivos de encabezado. Siéntase libre de usarlos en sus
.cc
archivos para evitar que aparezcan símbolos en sus archivos binarios. En algunos casos, cambiar todos los detalles internos de un.cc
archivo mostró una reducción del 10% en los tamaños binarios generados.Cambiar las opciones de visibilidad
En los compiladores más nuevos, puede seleccionar sus símbolos para que sean visibles o invisibles en los Objetos dinámicos compartidos (DSO). Idealmente, cambiar la visibilidad puede mejorar el rendimiento del compilador, las optimizaciones de tiempo de enlace (LTO) y los tamaños binarios generados. Si observa los archivos de encabezado STL en GCC, puede ver que se usa ampliamente. Para habilitar las opciones de visibilidad, debe cambiar su código por función, por clase, por variable y, lo que es más importante, por compilador.
Con la ayuda de la visibilidad, puede ocultar los símbolos que considera privados de los objetos compartidos generados. En GCC puede controlar la visibilidad de los símbolos pasando por defecto u oculto a la
-visibility
opción de su compilador. Esto es, en cierto sentido, similar al espacio de nombres sin nombre, pero de una manera más elaborada e intrusiva.Si desea especificar las visibilidades por caso, debe agregar los siguientes atributos a sus funciones, variables y clases:
La visibilidad predeterminada en GCC es predeterminada (pública), lo que significa que si compila lo anterior como un
-shared
método de biblioteca compartida ( ),foo2
y la clasefoo3
no será visible en otras TU (foo1
yfoo4
será visible). Si compila con-visibility=hidden
, solofoo1
será visible. Inclusofoo4
estaría oculto.Puede leer más sobre la visibilidad en el wiki de GCC .
fuente
Recomiendo estos artículos de "Juegos desde dentro, diseño y programación de juegos independientes":
De acuerdo, son bastante viejos: tendrás que volver a probar todo con las últimas versiones (o versiones disponibles) para obtener resultados realistas. De cualquier manera, es una buena fuente de ideas.
fuente
Una técnica que funcionó bastante bien para mí en el pasado: no compile varios archivos fuente C ++ de forma independiente, sino que genere un archivo C ++ que incluya todos los demás archivos, como este:
Por supuesto, esto significa que debe volver a compilar todo el código fuente incluido en caso de que cambie cualquiera de las fuentes, por lo que el árbol de dependencia empeora. Sin embargo, compilar múltiples archivos fuente como una unidad de traducción es más rápido (al menos en mis experimentos con MSVC y GCC) y genera binarios más pequeños. También sospecho que el compilador tiene más potencial para optimizaciones (ya que puede ver más código a la vez).
Esta técnica se rompe en varios casos; por ejemplo, el compilador rescatará en caso de que dos o más archivos fuente declaren una función global con el mismo nombre. Sin embargo, no pude encontrar esta técnica descrita en ninguna de las otras respuestas, por eso la menciono aquí.
Para lo que vale, el Proyecto KDE utilizó exactamente esta misma técnica desde 1999 para construir binarios optimizados (posiblemente para un lanzamiento). Se llamó al cambio al script de configuración de compilación
--enable-final
. Por interés arqueológico, desenterré la publicación que anunciaba esta característica: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2fuente
<core-count> + N
sublistas que se compilan en paralelo dondeN
hay algún número entero adecuado (dependiendo de la memoria del sistema y cómo se usa la máquina).Hay un libro completo sobre este tema, que se titula Diseño de software C ++ a gran escala (escrito por John Lakos).
Las plantillas anteriores al libro, por lo que al contenido de ese libro agregue "el uso de plantillas también puede hacer que el compilador sea más lento".
fuente
Simplemente vincularé a mi otra respuesta: ¿Cómo se reduce el tiempo de compilación y el tiempo de vinculación para proyectos de Visual C ++ (C ++ nativo)? . Otro punto que quiero agregar, pero que a menudo causa problemas es usar encabezados precompilados. Pero, por favor, úselos solo para partes que casi nunca cambian (como los encabezados del kit de herramientas GUI). De lo contrario, al final te costarán más tiempo del que te salvan.
Otra opción es, cuando trabaja con GNU make, activar la
-j<N>
opción:Usualmente lo tengo
3
puesto que tengo un doble núcleo aquí. Luego ejecutará compiladores en paralelo para diferentes unidades de traducción, siempre que no existan dependencias entre ellos. La vinculación no se puede hacer en paralelo, ya que solo hay un proceso de vinculación que vincula todos los archivos de objetos.Pero el enlazador en sí puede ser enhebrado, y esto es lo que hace el enlazador ELF . Es un código C ++ roscado optimizado que se dice que vincula los archivos de objetos ELF una magnitud más rápida que la anterior (y que en realidad se incluyó en binutils ).
GNU gold
ld
fuente
Aquí están algunas:
make -j2
es un buen ejemplo).-O1
que-O2
o-O3
).fuente
-j12
en general-j18
fueron mucho más rápidos que-j8
, tal como sugieres. Me pregunto cuántos núcleos puede tener antes de que el ancho de banda de la memoria se convierta en el factor limitante ...-j
con 2 veces el número de núcleos reales.Una vez que haya aplicado todos los trucos de código anteriores (declaraciones directas , reduciendo la inclusión de encabezados al mínimo en los encabezados públicos, introduciendo la mayoría de los detalles dentro del archivo de implementación con Pimpl ...) y nada más se puede obtener en términos de lenguaje, considere su sistema de compilación . Si usa Linux, considere usar distcc (compilador distribuido) y ccache (compilador de caché).
El primero, distcc, ejecuta el paso del preprocesador localmente y luego envía la salida al primer compilador disponible en la red. Requiere las mismas versiones de compilador y biblioteca en todos los nodos configurados en la red.
El último, ccache, es un caché del compilador. Nuevamente ejecuta el preprocesador y luego verifica con una base de datos interna (guardada en un directorio local) si ese archivo del preprocesador ya ha sido compilado con los mismos parámetros del compilador. Si lo hace, solo aparece el binario y la salida de la primera ejecución del compilador.
Ambos pueden usarse al mismo tiempo, de modo que si ccache no tiene una copia local, puede enviarlo a través de la red a otro nodo con distcc, o simplemente puede inyectar la solución sin más procesamiento.
fuente
Cuando salí de la universidad, el primer código C ++ digno de producción real que vi tenía estas directivas arcanas #ifndef ... #endif entre ellas donde se definieron los encabezados. Le pregunté al tipo que estaba escribiendo el código sobre estas cosas generales de una manera muy ingenua y me presentaron al mundo de la programación a gran escala.
Volviendo al punto, el uso de directivas para evitar definiciones de encabezado duplicadas fue lo primero que aprendí a la hora de reducir los tiempos de compilación.
fuente
Más RAM
Alguien habló sobre unidades de RAM en otra respuesta. Hice esto con un 80286 y Turbo C ++ (muestra la edad) y los resultados fueron fenomenales. Al igual que la pérdida de datos cuando la máquina se bloqueó.
fuente
Use declaraciones hacia adelante donde pueda. Si una declaración de clase solo usa un puntero o una referencia a un tipo, puede simplemente declararla hacia adelante e incluir el encabezado para el tipo en el archivo de implementación.
Por ejemplo:
Menos incluye significa mucho menos trabajo para el preprocesador si lo hace lo suficiente.
fuente
Podrías usar Unity Builds .
fuente
Utilizar
en la parte superior de los archivos de encabezado, por lo que si se incluyen más de una vez en una unidad de traducción, el texto del encabezado solo se incluirá y analizará una vez.
fuente
Solo para completar: una compilación puede ser lenta porque el sistema de compilación es estúpido y porque el compilador está tardando mucho en hacer su trabajo.
Lea Recursive Make Considered Damful (PDF) para una discusión de este tema en entornos Unix.
fuente
Actualiza tu computadora
Entonces tienes todas tus otras sugerencias típicas
fuente
Tuve una idea sobre el uso de una unidad de RAM . Resultó que para mis proyectos no hace mucha diferencia después de todo. Pero entonces son bastante pequeños todavía. ¡Intentalo! Me interesaría saber cuánto ayudó.
fuente
El enlace dinámico (.so) puede ser mucho más rápido que el enlace estático (.a). Especialmente cuando tienes una unidad de red lenta. Esto se debe a que tiene todo el código en el archivo .a que debe procesarse y escribirse. Además, un archivo ejecutable mucho más grande debe escribirse en el disco.
fuente
No se trata del tiempo de compilación, sino del tiempo de compilación:
Use ccache si tiene que reconstruir los mismos archivos cuando está trabajando en sus archivos de compilación
Usa ninja-build en lugar de make. Actualmente estoy compilando un proyecto con ~ 100 archivos fuente y todo está almacenado en caché por ccache. necesita 5 minutos, ninja menos de 1.
Puede generar sus archivos ninja de cmake con
-GNinja
.fuente
¿Dónde pasas tu tiempo? ¿Estás vinculado a la CPU? ¿Atado a la memoria? Disco atado? ¿Puedes usar más núcleos? Más RAM? ¿Necesitas RAID? ¿Simplemente desea mejorar la eficiencia de su sistema actual?
Bajo gcc / g ++, ¿has mirado a ccache ? Puede ser útil si está haciendo
make clean; make
mucho.fuente
Discos duros más rápidos.
Los compiladores escriben muchos (y posiblemente enormes) archivos en el disco. Trabaje con SSD en lugar del disco duro típico y los tiempos de compilación son mucho más bajos.
fuente
En Linux (y tal vez otros * NIX), realmente puede acelerar la compilación NO ESTRELLANDO en la salida y cambiando a otro TTY.
Aquí está el experimento: printf ralentiza mi programa
fuente
Los recursos compartidos de redes ralentizarán drásticamente su compilación, ya que la latencia de búsqueda es alta. Para algo como Boost, marcó una gran diferencia para mí, a pesar de que nuestro disco compartido de red es bastante rápido. El tiempo para compilar un programa Boost de juguete pasó de aproximadamente 1 minuto a 1 segundo cuando cambié de un recurso compartido de red a un SSD local.
fuente
Si tiene un procesador multinúcleo, tanto Visual Studio (2005 y posterior) como GCC admiten compilaciones multiprocesador. Es algo que debes habilitar si tienes el hardware, seguro.
fuente
Aunque no es una "técnica", no pude entender cómo los proyectos Win32 con muchos archivos fuente se compilaron más rápido que mi proyecto vacío "Hello World". Por lo tanto, espero que esto ayude a alguien como a mí.
En Visual Studio, una opción para aumentar los tiempos de compilación es la vinculación incremental ( / INCREMENTAL ). Es incompatible con la generación de código de tiempo de enlace ( / LTCG ), por lo tanto, recuerde deshabilitar el enlace incremental al hacer versiones de lanzamiento.
fuente
/INCREMENTAL
A partir de Visual Studio 2017, tiene la capacidad de tener algunas métricas del compilador sobre lo que lleva tiempo.
Agregue esos parámetros a C / C ++ -> Línea de comando (Opciones adicionales) en la ventana de propiedades del proyecto:
/Bt+ /d2cgsummary /d1reportTime
Puedes tener más información en esta publicación .
fuente
El uso de enlaces dinámicos en lugar de estáticos te hace compilar más rápido de lo que puedes sentir.
Si usa t Cmake, active la propiedad:
Build Release, el uso de enlaces estáticos puede ser más optimizado.
fuente