ejemplos descargar definicion compiler caracteristicas c++

descargar - c++ manual



¿Qué técnicas se pueden utilizar para acelerar los tiempos de compilación de C++? (24)

¿Qué técnicas se pueden utilizar para acelerar los tiempos de compilación de C ++?

Esta pregunta surgió en algunos comentarios al estilo de programación C ++ de Stack Overflow, y me interesa escuchar qué ideas hay.

He visto una pregunta relacionada, ¿Por qué la compilación de C ++ lleva tanto tiempo? , pero eso no proporciona muchas soluciones.

Los votos aquí tienen soporte de Visual Studio para compartir encabezados precompilados entre proyectos


Tecnicas de lenguaje

Idioma Pimpl

Eche un vistazo al lenguaje Pimpl here , y here , también conocido como un puntero opaco o clases de manejo. No solo acelera la compilación, sino que también aumenta la seguridad de excepción cuando se combina con una función de intercambio no lanzable . El lenguaje Pimpl le permite reducir las dependencias entre encabezados y reduce la cantidad de recompilación que debe hacerse.

Declaraciones Forward

Siempre que sea posible, utilice declaraciones hacia adelante . Si el compilador solo necesita saber que SomeIdentifier es una estructura o un puntero o lo que sea, no incluya la definición completa, lo que obliga al compilador a hacer más trabajo del que necesita. Esto puede tener un efecto de cascada, por lo que es más lento de lo necesario.

Las secuencias de I/O son particularmente conocidas por ralentizar las construcciones. Si los necesita en un archivo de encabezado, intente #incluyendo <iosfwd> lugar de <iostream> y #incluya el encabezado <iostream> en el archivo de implementación solamente. El <iosfwd> solo contiene declaraciones hacia adelante. Desafortunadamente, los otros encabezados estándar no tienen un encabezado de declaración respectivo.

Prefiere paso por referencia a paso por valor en firmas de funciones. Esto eliminará la necesidad de #incluir las definiciones de tipo respectivas en el archivo de encabezado y solo deberá declarar hacia adelante el tipo. Por supuesto, prefiera las referencias const a las referencias no constantes para evitar errores ocultos, pero esto es un problema para otra pregunta.

Condiciones de la Guardia

Utilice las condiciones de guarda para evitar que los archivos de encabezado se incluyan más de una vez en una sola unidad de traducción.

#pragma once #ifndef filename_h #define filename_h // Header declarations / definitions #endif

Al usar tanto el pragma como el ifndef, se obtiene la portabilidad de la solución de macro simple, así como la optimización de la velocidad de compilación que algunos compiladores pueden hacer en presencia de la directiva pragma once .

Reducir la interdependencia

Cuanto más modular y menos interdependiente sea el diseño de su código, en general, con menor 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 de qué hacer 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 obtener una ventaja inicial en la compilación de otro archivo con el mismo conjunto de encabezados.

Tenga cuidado de que solo incluya elementos raramente cambiados en los encabezados precompilados, o podría terminar haciendo reconstrucciones completas más a menudo de lo necesario. Este es un buen lugar para los encabezados de STL y otros archivos de biblioteca incluidos.

ccache es otra utilidad que aprovecha las técnicas de almacenamiento en caché para acelerar el proceso.

Usa el paralelismo

Muchos compiladores / IDE admiten el uso de múltiples núcleos / CPU para realizar la compilación simultáneamente. En GNU Make (generalmente utilizado con GCC), use la opción -j [N] . En Visual Studio, hay una opción en las preferencias que le permite construir múltiples proyectos en paralelo. También puede usar la opción /MP para el paralelismo a nivel de archivo, en lugar de solo el paralelismo a nivel de proyecto.

Otras utilidades paralelas:

Utilice un nivel de optimización más bajo

Cuanto más intente optimizar el compilador, más difícil tendrá que trabajar.

Bibliotecas compartidas

Mover su código menos frecuentemente modificado a bibliotecas puede reducir el tiempo de compilación. Al usar bibliotecas compartidas ( .so o .dll ), también puede reducir el tiempo de enlace.

Consigue una computadora más rápida

Más RAM, discos duros más rápidos (incluidos los SSD) y más CPU / núcleos marcarán la diferencia en la velocidad de compilación.


¿Dónde estás gastando tu tiempo? ¿Estás CPU enlazado? Memoria enlazada? Disco enlazado? ¿Puedes usar más núcleos? ¿Más memoria RAM? ¿Necesita RAID? ¿Simplemente desea mejorar la eficiencia de su sistema actual?

.

Bajo gcc / g ++, ¿has mirado ccache ? Puede ser útil si estás haciendo make_clean _; _ make mucho.


Aquí están algunas:

  • Utilice todos los núcleos de procesador al iniciar un trabajo de compilación múltiple ( make -j2 es un buen ejemplo).
  • Desactive o -O1 las optimizaciones (por ejemplo, GCC es mucho más rápido con -O1 que con -O2 o -O2 ).
  • Utilice encabezados precompilados .

Aunque no es una "técnica", no pude averiguar cómo proyecta Win32 con muchos archivos de origen compilados más rápido que mi proyecto vacío "Hola Mundo". Por lo tanto, espero que esto ayude a alguien como lo hizo a mí.

En Visual Studio, una opción para aumentar los tiempos de compilación es Incremental Linking ( / INCREMENTAL ). Es incompatible con la generación de código de tiempo de enlace ( / LTCG ), así que recuerde desactivar el enlace incremental cuando realice compilaciones de lanzamiento.


Cuando salí de la universidad, el primer código real de C ++ digno de producción que vi tenía estas directivas #ifndef ... #endif entre ellas donde se definían los encabezados. Le pregunté al chico que estaba escribiendo el código sobre estas cosas generales de una manera muy ingenua y fue introducido al mundo de la programación a gran escala.

Volviendo al punto, el uso de directivas para evitar definiciones de encabezados duplicados fue lo primero que aprendí a la hora de reducir los tiempos de compilación.


Discos duros más rápidos.

Los compiladores escriben muchos archivos (y posiblemente enormes) en el disco. Trabajar con SSD en lugar del disco duro típico y los tiempos de compilación son mucho más bajos.


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, se debe escribir en el disco un archivo ejecutable mucho más grande.


En Linux (y quizás en otros * NIXes), realmente puede acelerar la compilación NO ESTRENANDO en la salida y cambiando a otro TTY.

Aquí está el experimento: printf ralentiza mi programa


Hay un libro completo sobre este tema, que se titula Diseño de software C ++ a gran escala (escrito por John Lakos).

El libro precede a las plantillas, por lo que al contenido de ese libro agregue "el uso de plantillas también puede hacer que el compilador sea más lento".


Las redes compartidas ralentizarán drásticamente tu construcción, ya que la latencia de búsqueda es alta. Para algo como Boost, fue una gran diferencia para mí, aunque nuestra unidad de red compartida es bastante rápida. 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.


Más memoria RAM.

Alguien habló sobre las unidades de memoria RAM en otra respuesta. Hice esto con un 80286 y un Turbo C ++ (muestra la edad) y los resultados fueron fenomenales. Como fue la pérdida de datos cuando la máquina se estrelló.


No sobre el tiempo de compilación, sino sobre el 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 de origen y todo está almacenado en caché por ccache. Haz necesidades 5 minutos, ninja menos de 1.

Puedes generar tus archivos ninja desde cmake con -GNinja .



Recomiendo estos artículos de "Juegos desde adentro, Diseño y programación de juegos independientes":

Por supuesto, son bastante antiguas: 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.


Si tiene un procesador multinúcleo, tanto Visual Studio (2005 y posterior) como GCC compatibles con compilaciones multiprocesador. Es algo para habilitar si tienes el hardware, seguro.


Solo enlazaré con mi otra respuesta: ¿Cómo USTED reduce el tiempo de compilación y el tiempo de vinculación para los proyectos de Visual C ++ (C ++ nativo)? . Otro punto que quiero agregar, pero que causa problemas a menudo es el uso de encabezados precompilados. Pero, por favor, solo úselos para las partes que casi nunca cambian (como los encabezados de herramientas de GUI). De lo contrario, te costarán más tiempo del que te ahorran al final.

Otra opción es, cuando trabaja con GNU make, activar la opción -j<N> :

-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.

Normalmente lo tengo a las 3 ya que tengo un doble núcleo aquí. A continuación, ejecutará compiladores en paralelo para diferentes unidades de traducción, siempre que no haya 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 roscado, y esto es lo que hace el enlazador ELF GNU gold . Está optimizado para el código C ++ de subprocesos que se dice que vincula los archivos de objetos ELF una magnitud más rápida que el antiguo ld (y en realidad fue incluido en binutils ).


Solo para estar completo: una compilación puede ser lenta porque el sistema de compilación es estúpido y porque el compilador está tardando mucho tiempo en hacer su trabajo.

Lea Recursive Make Considered Harmful (PDF) para una discusión de este tema en entornos Unix.


Trabajo en el proyecto STAPL, que es una biblioteca de C ++ con muchas plantillas. De vez en cuando, tenemos que revisar todas las técnicas para reducir el tiempo de compilación. Aquí, he resumido las técnicas que utilizamos. Algunas de estas técnicas ya están listadas arriba:

Encontrar las secciones que más tiempo consumen.

Aunque no existe una correlación probada entre la longitud de los símbolos y el tiempo de compilación, hemos observado que los tamaños de símbolo promedio más pequeños pueden mejorar el tiempo de compilación en todos los compiladores. Así que tu primer objetivo es encontrar los símbolos más grandes en tu código.

Método 1 - Ordenar símbolos en función del tamaño

Puede usar el comando nm para enumerar los símbolos según sus tamaños:

nm --print-size --size-sort --radix=d YOUR_BINARY

En este comando, --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 dividir la clase correspondiente e intente rediseñarlo factorizando las partes sin plantilla en una clase base, o dividiendo la clase en varias clases.

Método 2 - Ordenar símbolos en función de la longitud

Puede ejecutar el comando nm normal y canalizarlo a su script favorito ( AWK , Python , etc.) para ordenar los símbolos según su longitud . Según nuestra experiencia, este método identifica los problemas más grandes que hacen que los candidatos sean mejores que el método 1.

Método 3 - Usa la noche del templo

" Templight es una herramienta basada en Clang para perfilar el tiempo y el consumo de memoria de las instancias de plantilla y para realizar sesiones de depuración interactivas para obtener introspección en el proceso de creación de instancias de plantilla".

Puede instalar Templight revisando LLVM y Clang ( instructions ) y aplicando el parche de Templight en él. La configuración predeterminada para LLVM y Clang está en depuración y aserciones, y esto puede afectar significativamente el tiempo de compilación. Parece que Templight necesita ambas cosas, así que tienes que usar la configuración predeterminada. El proceso de instalación de LLVM y Clang debería tomar aproximadamente una hora.

Después de aplicar el parche, puede usar templight++ ubicado en la carpeta de compilación que especificó en 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 su CXXFLAGS en su Makefile o a sus opciones de línea de comando:

CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

O

templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

Una vez que se haya completado 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 instructions para instalar templight-convert. Usualmente usamos la salida de callgrind. También puede usar la salida de GraphViz si su proyecto es pequeño:

$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace $ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot

El archivo de callgrind generado se puede abrir con kcachegrind en el que puede rastrear la mayor parte del tiempo / la memoria que consume instancias.

Reduciendo el número de instancias de plantilla.

Aunque no hay una solución exacta para reducir el número de instancias de plantillas, hay algunas pautas que pueden ayudar:

Clases de refactor con más de un argumento de plantilla.

Por ejemplo, si tienes una clase,

template <typename T, typename U> struct foo { };

y tanto T como U pueden tener 10 opciones diferentes, ha aumentado las posibles instancias de plantilla de esta clase a 100. Una forma de resolver esto es abstraer la parte común del código a una clase diferente. El otro método es utilizar la inversión de herencia (invirtiendo la jerarquía de clases), pero asegúrese de que sus objetivos de diseño no se vean comprometidos antes de usar esta técnica.

Refactorizar código sin plantilla para unidades de traducción individuales

Usando esta técnica, puede compilar la sección común una vez y vincularla con sus otras unidades de traducción (unidades de traducción) más adelante.

Usar instanciaciones de plantillas externas (desde C ++ 11)

Si conoce todas las posibles instancias de una clase, puede utilizar esta técnica para compilar todos los casos en una unidad de traducción diferente.

Por ejemplo, en:

enum class PossibleChoices = {Option1, Option2, Option3} template <PossibleChoices pc> struct foo { };

Sabemos que esta clase puede tener tres posibles instancias:

template class foo<PossibleChoices::Option1>; template class foo<PossibleChoices::Option2>; template class foo<PossibleChoices::Option3>;

Coloque 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:

extern template class foo<PossibleChoices::Option1>; extern template class foo<PossibleChoices::Option2>; extern template class foo<PossibleChoices::Option3>;

Esta técnica puede ahorrarle tiempo si está compilando diferentes pruebas con un conjunto común de instancias.

NOTA: MPICH2 ignora la creación de instancias explícita en este punto y siempre compila las clases creadas en todas las unidades de compilación.

Usa las unidades de la unidad.

La idea principal de las compilaciones unitarias es incluir todos los archivos .cc que use en un archivo y compilar ese archivo solo una vez. Usando este método, puede evitar reinstituir secciones comunes de diferentes archivos y si su proyecto incluye muchos archivos comunes, probablemente también ahorrará en accesos de disco.

Como ejemplo, supongamos que tiene tres archivos foo1.cc , foo2.cc , foo3.cc y todos incluyen tuple de STL . Puede crear un foo-all.cc que se parece a:

#include "foo1.cc" #include "foo2.cc" #include "foo3.cc"

Usted 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ía el paralelismo en sus compilaciones (ya no puede compilar los tres archivos al mismo tiempo).

Además, si alguno de estos archivos requiere mucha memoria, es posible que se quede sin memoria antes de que finalice la compilación. En algunos compiladores, como GCC , esto podría provocar el ICE (Internal Compiler Error) de su compilador por falta de memoria. Así que no uses esta técnica a menos que conozcas todos los pros y los contras.

Encabezados precompilados

Los encabezados precompilados (PCH) pueden ahorrarle mucho tiempo en la compilación al compilar 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 regular. Por ejemplo, en GCC:

$ g++ YOUR_HEADER.hpp

Esto generará un YOUR_HEADER.hpp.gch file ( .gch es la extensión para los archivos PCH en GCC) en la misma carpeta. Esto significa que si incluye YOUR_HEADER.hpp en algún otro archivo, el compilador usará YOUR_HEADER.hpp.gch lugar de YOUR_HEADER.hpp en la misma carpeta anterior.

Hay dos problemas con esta técnica:

  1. Debe asegurarse de que los archivos de encabezado que se precompilan sean estables y no vayan a cambiar ( siempre puede cambiar su makefile )
  2. Solo puede incluir un PCH por unidad de compilación (en la mayoría de los compiladores). Esto significa que si tiene más de un archivo de encabezado para precompilar, debe incluirlos en un archivo (por ejemplo, all-my-headers.hpp ). Pero eso significa que tienes que incluir el nuevo archivo en todos los lugares. Afortunadamente, GCC tiene una solución para este problema. Usa -include y dale el nuevo archivo de encabezado. Puedes comas separar diferentes archivos usando esta técnica.

Por ejemplo:

g++ foo.cc -include all-my-headers.hpp

Utilice espacios de nombres anónimos o anónimos

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 unidades de traducción (unidades de traducción o compilación). Los compiladores generalmente generan nombres únicos para espacios de nombres sin nombre. Esto significa que si tienes un archivo foo.hpp:

namespace { template <typename T> struct foo { }; } // Anonymous namespace using A = foo<int>;

Y resulta que incluye este archivo en dos TU (dos archivos .cc y los compila por separado). Las dos instancias de la plantilla foo no serán las mismas. Esto viola la regla de una definición (ODR). Por la misma razón, no se recomienda el uso de espacios de nombres sin nombre en los archivos de encabezado. Siéntase libre de usarlos en sus archivos .cc para evitar que aparezcan símbolos en sus archivos binarios. En algunos casos, cambiar todos los detalles internos de un archivo .cc mostró una reducción del 10% en los tamaños binarios generados.

Cambio de opciones de visibilidad

En los compiladores más nuevos, puede seleccionar que sus símbolos sean visibles o invisibles en los objetos compartidos dinámicos (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 de STL en GCC, puede ver que se usa ampliamente. Para habilitar las opciones de visibilidad, necesita 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 considere privados de los objetos compartidos generados. En GCC puede controlar la visibilidad de los símbolos pasando de forma predeterminada u oculta a la opción -visibility de su compilador. En cierto sentido, esto es 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:

__attribute__((visibility("default"))) void foo1() { } __attribute__((visibility("hidden"))) void foo2() { } __attribute__((visibility("hidden"))) class foo3 { }; void foo4() { }

La visibilidad predeterminada en GCC es predeterminada (pública), lo que significa que si compila lo anterior como un método de biblioteca compartida ( -shared ), foo2 y la clase foo3 no serán visibles en otras TU ( foo1 y foo4 serán visibles). Si compilas con -visibility=hidden , solo foo1 será visible. Incluso foo4 estaría oculto.

Puedes leer más sobre la visibilidad en la wiki de GCC .


Tuve una idea sobre el uso de una unidad de memoria 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ó.


Una técnica que me funcionó bastante bien en el pasado: no compile varios archivos de origen de C ++ de forma independiente, sino que genere un archivo de C ++ que incluya todos los demás archivos, como este:

// myproject_all.cpp // Automatically generated file - don''t edit this by hand! #include "main.cpp" #include "mainwindow.cpp" #include "filterdialog.cpp" #include "database.cpp"

Por supuesto, esto significa que tiene que volver a compilar todo el código fuente incluido en caso de que alguna de las fuentes cambie, por lo que el árbol de dependencia empeora. Sin embargo, la compilación de varios archivos fuente como una unidad de traducción es más rápida (al menos en mis experimentos con MSVC y GCC) y genera binarios más pequeños. También sospecho que al compilador se le da un mayor potencial de optimización (ya que puede ver más código a la vez).

Esta técnica se rompe en varios casos; por ejemplo, el compilador se rescatará en caso de que dos o más archivos de origen 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 lo menciono aquí.

Para lo que vale la pena, el Proyecto KDE utilizó esta misma técnica desde 1999 para construir binarios optimizados (posiblemente para un lanzamiento). El cambio al script de configuración de compilación se denominó --enable-final . Por interés arqueológico desenterré la publicación que anunció esta característica: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2


Una vez que haya aplicado todos los trucos de código anteriores (reenviar declaraciones, reducir la inclusión de encabezados al mínimo en los encabezados públicos, empujar 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é de compilador. Ejecuta nuevamente el preprocesador y luego verifica con una base de datos interna (contenida en un directorio local) si ese archivo del preprocesador ya se ha compilado con los mismos parámetros del compilador. Si lo hace, simplemente 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 enviarla a través de la red a otro nodo con distcc, o bien puede inyectar la solución sin procesamiento adicional.


Usa declaraciones hacia adelante donde puedas. Si una declaración de clase solo usa un puntero o una referencia a un tipo, simplemente puede reenviar declararlo e incluir el encabezado para el tipo en el archivo de implementación.

Por ejemplo:

// T.h class Class2; // Forward declaration class T { public: void doSomething(Class2 &c2); private: Class2 *m_Class2Ptr; }; // T.cpp #include "Class2.h" void Class2::doSomething(Class2 &c2) { // Whatever you want here }

Menos incluye significa mucho menos trabajo para el preprocesador si lo haces lo suficiente.


Utilizar

#pragma once

en la parte superior de los archivos de encabezado, de modo 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.


  • Actualiza tu computadora

    1. Obtenga un quad core (o un sistema de doble quad)
    2. Obtener un montón de memoria RAM.
    3. Use una unidad de RAM para reducir drásticamente los retrasos de E / S de archivos. (Hay compañías que hacen unidades de RAM IDE y SATA que actúan como discos duros).
  • Entonces tienes todas tus otras sugerencias típicas

    1. Use encabezados precompilados si están disponibles.
    2. Reduzca la cantidad de acoplamiento entre las partes de su proyecto. Por lo general, cambiar un archivo de encabezado no debería requerir la recompilación de todo el proyecto.