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:
nm --print-size --size-sort --radix=d YOUR_BINARY
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 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 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:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
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,
template <typename T, typename U>
struct foo { };
y los dos T
y U
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:
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>;
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:
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 instanciación explícita en este punto y siempre compila las clases instanciadas en todas las unidades de compilación.
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 incluyen tuple
desde STL . Puede crear uno foo-all.cc
que se vea así:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
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:
$ g++ YOUR_HEADER.hpp
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 incluye YOUR_HEADER.hpp
en algún otro archivo, el compilador usará su en YOUR_HEADER.hpp.gch
lugar de YOUR_HEADER.hpp
en la misma carpeta antes.
Hay dos problemas con esta técnica:
- Debe asegurarse de que los archivos de encabezado que se precompilan son estables y no van a cambiar ( siempre puede cambiar su archivo MAKE )
- Solo puede incluir una PCH por unidad de compilación (en la mayoría de los compiladores). Esto significa que si tiene que precompilar más de un archivo de encabezado, debe incluirlos en un archivo (por ejemplo,
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:
g++ foo.cc -include all-my-headers.hpp
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:
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 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:
__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 -shared
método de biblioteca compartida ( ), foo2
y la clase foo3
no será visible en otras TU ( foo1
y foo4
será visible). Si compila con -visibility=hidden
, solo foo1
será visible. Incluso foo4
estaría oculto.
Puede leer más sobre la visibilidad en el wiki de GCC .