Red de conocimiento informático - Material del sitio web - Cómo acelerar la compilación de código C++

Cómo acelerar la compilación de código C++

El código C++ siempre ha sido conocido por su alto rendimiento en tiempo de ejecución, pero cuando se trata de velocidad de compilación, solo puede considerarse discreto. Por ejemplo, para el código fuente en el que estoy trabajando ahora, incluso si uso Incredibuild para movilizar casi cien máquinas, una compilación completa tomará cuatro horas, ¡lo cual es terrible! Aunque generalmente no es necesario realizar una compilación local completa para el desarrollo normal, compilar algunos proyectos relacionados es suficiente para esperar mucho tiempo (los extranjeros llaman a este mono caminando, lo cual es bastante vívido). Piense en la escena en la que trabajó en un solo núcleo de 2,8 GHZ durante varios años: coloque un libro frente a usted, haga clic en el botón de compilación, simplemente baje la cabeza y lea un rato ~~~ Es difícil mirar hacia atrás. pasado.

Como puedes imaginar, si no se toma en serio la velocidad de compilación, puede convertirse en un cuello de botella en el proceso de desarrollo. Entonces, ¿por qué la velocidad de compilación de C++ es tan lenta?

Creo que una de las razones más importantes es el modelo básico de compilación "archivo de encabezado-archivo fuente" de C++:

Como unidad de compilación, cada archivo fuente puede contener cientos o incluso Miles de archivos de encabezado, que se leerán desde el disco y se analizarán una vez por unidad de compilación.

Cada unidad de compilación produce un archivo obj, por lo que estos archivos obj están vinculados entre sí, con poca paralelización.

Aquí, el problema radica en la carga y análisis repetidos de innumerables archivos de encabezado, así como en las operaciones intensivas en el disco.

A continuación se presentarán algunos métodos para acelerar la compilación desde diferentes ángulos, centrándose principalmente en los problemas clave anteriores.

I. Perspectiva del código

Utilice la declaración previa en el archivo de encabezado en lugar de incluir directamente el archivo de encabezado.

No crea que acaba de agregar un archivo de encabezado adicional. Debido a la naturaleza de "inclusión" del archivo de encabezado, su efecto puede amplificarse infinitamente. Por lo tanto, haga todo lo posible para que sus archivos de encabezado sean concisos. Muchas veces es más doloroso declarar una clase por adelantado en un espacio de nombres que incluirla directamente, por lo que también debe resistir esta tentación de crear condiciones predeclaradas para los miembros de la clase, parámetros de funciones, etc. Utilice referencias y punteros; siempre que es posible.

Usar el modo Pimpl

Pimpl se llama implementación privada; tradicionalmente, la interfaz y la implementación de las clases de C++ se confunden, pero Pimpl hace que la interfaz y la implementación de la clase se vuelvan completamente separables. De esta manera, mientras la interfaz pública *** de la clase permanezca sin cambios, los cambios en la implementación de la clase siempre solo requieren compilar cpp al mismo tiempo, y los archivos de encabezado proporcionados por la clase también serán más simplificados;

Altamente modular

Modularidad significa bajo acoplamiento, lo que significa minimizar la interdependencia. En realidad, aquí hay dos significados. El primero es de archivo a archivo, y las modificaciones a un archivo de encabezado deben intentar no provocar la compilación de otros archivos; el segundo es de proyecto a proyecto, y las modificaciones a un proyecto deben intentar no provocar la compilación de demasiados; otros proyectos. Esto requiere que el contenido del archivo de encabezado o del proyecto sea único y no abarrote todo, lo que provocará dependencias innecesarias. Supongo que a esto también se le podría llamar cohesión.

Tome los archivos de encabezado como ejemplo. No coloque dos clases no relacionadas o definiciones de macros que tengan poca relación entre sí en un archivo de encabezado. Los contenidos deben ser lo más consistentes posible para que los archivos que los contienen no contengan contenido no deseado. Recuerdo una cosa que hicimos: dividimos los archivos de encabezado más "calientes" de nuestro código en archivos más pequeños e independientes, y los resultados fueron impresionantes.

De hecho, lo que hicimos el año pasado para separar muchas DLL en UI y partes principales también logró el mismo efecto: mejorar la eficiencia del desarrollo.

Eliminar archivos de encabezado redundantes

Parte del código se ha desarrollado y mantenido durante diez años y ha sido procesado por innumerables personas, por lo que es probable que contenga archivos de encabezado inútiles o inclusiones duplicadas, y Es muy necesario eliminar estas inclusiones redundantes. Por supuesto, esto es principalmente para cpp, porque para un archivo de encabezado, es difícil determinar si una de las inclusiones es redundante. Depende de si finalmente se usa la unidad de compilación, por lo que se puede usar una unidad de compilación mientras se usa otra. No se utiliza en una unidad de compilación.

Escribimos un script Perl para eliminar automáticamente estos archivos de encabezado redundantes, lo que eliminó hasta más de 5000 inclusiones en un solo proyecto.

Presta especial atención a las líneas y las plantillas

Estos son dos de los mecanismos más "avanzados" en C++, pero nos obligan a incluir la implementación en el archivo de encabezado, lo que puede causar Los problemas al agregar encabezados se ven afectados negativamente. Esto puede aumentar significativamente el tamaño del archivo de encabezado, lo que ralentiza la compilación. Se deben sopesar los pros y los contras antes de usarlos.

II.Consejos generales

Archivos de encabezado precompilados (PCH)

Coloque algunos archivos de encabezado de uso común pero que se modifican con poca frecuencia en archivos de encabezado precompilados. De esta manera, al menos dentro de un único proyecto, no es necesario cargar y analizar el mismo archivo de encabezado una y otra vez en cada unidad de compilación.

Unity Build

Unity Build es muy simple: coloca todos los archivos cpp en un archivo cpp (all.cpp) y luego compila solo all.cpp. De esta manera, solo tenemos una unidad de compilación, lo que significa que no es necesario cargar y analizar el mismo archivo de encabezado una y otra vez, y como solo genera un archivo obj, no es necesario vincularlo a ese archivo al compilar. Además, dado que solo hay un archivo obj, no se requieren operaciones intensivas en el disco al vincular, lo que se estima es una mejora de 10 veces, así que mire este video para ver cómo funciona y qué tan rápido es.

Caché

Caché del compilador, al almacenar en caché los resultados de la última compilación, reconstruyendo manteniendo los mismos resultados, mejorando enormemente la velocidad. Sabemos que si se compila, el sistema comparará el tiempo del código fuente y el código de destino para decidir si recompilar un archivo. Este método en realidad no es completamente confiable (como verificar la versión anterior del código desde svn). Y el principio de juicio de ccache es el contenido del documento, que es relativamente más confiable. Desafortunadamente, Visual Studio no admite esto actualmente: si se pudiera agregar un nuevo comando entre la compilación y la reconstrucción, como la compilación en caché, entonces la reconstrucción podría eliminarse en gran medida.

No tenga demasiados directorios de inclusión adicionales

El compilador buscará los archivos de encabezado incluidos para encontrarlos según los directorios de inclusión que proporcione. Como puede imaginar, si proporcionó 100 directorios de inclusión y un archivo de encabezado en particular estaba en el directorio número 100, encontrarlo sería muy problemático. Organice sus directorios de inclusión e intente mantenerlos lo más concisos posible.

3. Recursos de compilación

Para mejorar la velocidad, reduzca las tareas o reduzca la mano de obra. Los dos aspectos mencionados anteriormente son para reducir las tareas, en términos de velocidad de compilación. , la mano de obra sigue desempeñando un papel muy importante.

Compilación paralela

Compre una CPU de 4 u 8 núcleos. Cada vez que construya, se compilarán 8 archivos en paralelo. Es genial ver esta velocidad. Si su jefe no está de acuerdo, enséñele este artículo: El hardware es barato, los programadores son caros

Mejores discos

Sabemos que las velocidades de compilación son lentas. Una gran parte de la razón son las operaciones del disco. por lo que además de minimizar las operaciones del disco, lo que podemos hacer es acelerar el disco. Por ejemplo, cuando los 8 núcleos anteriores funcionan juntos, es probable que el disco sea el mayor cuello de botella. Utilice discos de 15000 rpm, SSD o RAID0, en definitiva, más rápido, mejor.

Compilación distribuida

El rendimiento de una máquina siempre es limitado. Solo se pueden resolver fundamentalmente utilizando recursos de CPU inactivos en la red y un servidor de compilación dedicado a la compilación para ayudarlo a compilar. El problema de la velocidad de compilación. Si piensas en cómo un proyecto que originalmente tardó más de una hora en construirse se puede completar en 2 minutos, ¡sabrás que no puedes prescindir de él!

El paralelismo realmente se puede hacer.

Esta es una situación extrema. ¿Qué pasa si usas Incredibuild pero aún no estás satisfecho con la velocidad de la compilación final? De hecho, mientras pienses diferente, la velocidad de compilación aún puede dar un salto cualitativo, siempre que tengas suficientes máquinas:

Supongamos que tienes la solución A y la solución B, y B depende de A, por lo que Tienes que construir B después de A.

La construcción de A y B toma 1 hora cada uno, por lo que el tiempo total es 2 horas. ¿Pero B tiene que construirse después de A? Si lo piensas por un momento, obtendrás la siguiente solución:

De esta manera, al hacer la construcción de A paralela a la compilación de B, y finalmente vincular el proyecto en B, todo el La velocidad de compilación debe controlarse en 1 hora y 15 minutos dentro.

Empieza a construir A y B al mismo tiempo.

La construcción de A tiene éxito, mientras que la construcción de B falla, pero sólo en el último enlace.

Vuelva a vincular el proyecto en B.