La creación de instancias explícita permite reducir los tiempos de compilación y el tamaño de los objetos
Estos son los principales beneficios que puede proporcionar. Provienen de los dos efectos siguientes que se describen en detalle en las secciones siguientes:
- eliminar definiciones de los encabezados para evitar que las herramientas de compilación reconstruyan los incluidos
- redefinición de objetos
Eliminar definiciones de encabezados
La creación de instancias explícita le permite dejar definiciones en el archivo .cpp.
Cuando la definición está en el encabezado y la modificas, un sistema de compilación inteligente recompilaría todos los includers, que podrían ser docenas de archivos, haciendo que la compilación sea insoportablemente lenta.
Poner definiciones en archivos .cpp tiene la desventaja de que las bibliotecas externas no pueden reutilizar la plantilla con sus propias clases nuevas, pero "Eliminar definiciones de los encabezados incluidos pero también exponer plantillas a una API externa" a continuación muestra una solución.
Vea ejemplos concretos a continuación.
Ganancias de redefinición de objetos: comprensión del problema
Si simplemente define completamente una plantilla en un archivo de encabezado, cada unidad de compilación que incluye ese encabezado termina compilando su propia copia implícita de la plantilla para cada uso de argumento de plantilla diferente realizado.
Esto significa mucho tiempo de compilación y uso de disco inútil.
Aquí está un ejemplo concreto, en el que tanto main.cpp
y notmain.cpp
definen implícitamente MyTemplate<int>
debido a su uso en esos archivos.
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
#endif
notmain.hpp
#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP
int notmain();
#endif
GitHub aguas arriba .
Compile y vea símbolos con nm
:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate
Salida:
notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
De man nm
, vemos que W
significa símbolo débil, que GCC eligió porque se trata de una función de plantilla. Símbolo débil significa que el código generado implícitamente para MyTemplate<int>
se compiló en ambos archivos.
La razón por la que no explota en el momento del enlace con múltiples definiciones es que el enlazador acepta múltiples definiciones débiles y solo elige una de ellas para ponerla en el ejecutable final.
Los números en la salida significan:
0000000000000000
: dirección dentro de la sección. Este cero se debe a que las plantillas se colocan automáticamente en su propia sección
0000000000000017
: tamaño del código generado para ellos
Podemos ver esto un poco más claramente con:
objdump -S main.o | c++filt
que termina en:
Disassembly of section .text._ZN10MyTemplateIiE1fEi:
0000000000000000 <MyTemplate<int>::f(int)>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
c: 89 75 f4 mov %esi,-0xc(%rbp)
f: 8b 45 f4 mov -0xc(%rbp),%eax
12: 83 c0 01 add $0x1,%eax
15: 5d pop %rbp
16: c3 retq
y _ZN10MyTemplateIiE1fEi
es el nombre destrozado del MyTemplate<int>::f(int)>
que c++filt
decidió no deshacerse.
De modo que vemos que se genera una sección separada para cada instanciación de un método y que cada uno de ellos ocupa, por supuesto, espacio en los archivos de objeto.
Soluciones al problema de redefinición de objetos
Este problema puede evitarse mediante la creación de instancias explícita y:
mantenga la definición en hpp y agregue extern template
hpp para los tipos que se van a instanciar explícitamente.
Como se explica en: el uso de una plantilla extern (C ++ 11) extern template
evita que las unidades de compilación creen instancias de una plantilla completamente definida, excepto nuestra instanciación explícita. De esta manera, solo nuestra instanciación explícita se definirá en los objetos finales:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
extern template class MyTemplate<int>;
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
Abajo:
- si es una biblioteca de solo encabezado, obliga a los proyectos externos a hacer su propia instanciación explícita. Si no es una biblioteca de solo encabezados, esta solución probablemente sea la mejor.
- si el tipo de plantilla está definido en su propio proyecto y no un tipo integrado
int
, parece que se ve obligado a agregar la inclusión para él en el encabezado, una declaración hacia adelante no es suficiente: plantilla externa y tipos incompletos Esto aumenta las dependencias del encabezado un poco.
moviendo la definición en el archivo cpp, deje solo la declaración en hpp, es decir, modifique el ejemplo original para que sea:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
template class MyTemplate<int>;
Desventaja: los proyectos externos no pueden usar su plantilla con sus propios tipos. También está obligado a crear una instancia explícita de todos los tipos. Pero tal vez esto sea una ventaja, ya que los programadores no lo olvidarán.
mantenga la definición en hpp y agregue extern template
cada includer:
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
extern template class MyTemplate<int>;
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
extern template class MyTemplate<int>;
int notmain() { return MyTemplate<int>().f(1); }
Desventaja: todos los includers tienen que agregarlos extern
a sus archivos CPP, lo que los programadores probablemente olvidarán hacer.
Con cualquiera de esas soluciones, nm
ahora contiene:
notmain.o
U MyTemplate<int>::f(int)
main.o
U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)
Así que vemos tiene solamente mytemplate.o
tiene una compilación de MyTemplate<int>
la forma deseada, mientras que notmain.o
y main.o
no lo hacen porque U
los medios no definidos.
Elimine las definiciones de los encabezados incluidos, pero también exponga las plantillas de una API externa en una biblioteca de solo encabezados
Si su biblioteca no es solo de encabezado, el extern template
método funcionará, ya que el uso de proyectos solo vinculará a su archivo de objeto, que contendrá el objeto de la instanciación de plantilla explícita.
Sin embargo, para bibliotecas solo de encabezado, si desea ambos:
- acelera la compilación de tu proyecto
- exponer los encabezados como una API de biblioteca externa para que otros la usen
entonces puedes probar una de las siguientes opciones:
-
mytemplate.hpp
: definición de plantilla
mytemplate_interface.hpp
: declaración de plantilla que solo coincide con las definiciones de mytemplate_interface.hpp
, sin definiciones
mytemplate.cpp
: incluir mytemplate.hpp
y crear instancias explícitas
main.cpp
y en cualquier otro lugar del código base: incluir mytemplate_interface.hpp
, nomytemplate.hpp
-
mytemplate.hpp
: definición de plantilla
mytemplate_implementation.hpp
: incluye mytemplate.hpp
y agrega extern
a cada clase que se instanciará
mytemplate.cpp
: incluir mytemplate.hpp
y crear instancias explícitas
main.cpp
y en cualquier otro lugar del código base: incluir mytemplate_implementation.hpp
, nomytemplate.hpp
O incluso mejor quizás para varios encabezados: cree una carpeta intf
/ impl
dentro de su includes/
carpeta y utilícela mytemplate.hpp
como nombre siempre.
El mytemplate_interface.hpp
enfoque se ve así:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
#include "mytemplate_interface.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
#endif
mytemplate_interface.hpp
#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate_interface.hpp"
int main() {
std::cout << MyTemplate<int>().f(1) << std::endl;
}
Compila y ejecuta:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o
Salida:
2
Probado en Ubuntu 18.04.
Módulos C ++ 20
https://en.cppreference.com/w/cpp/language/modules
Creo que esta función proporcionará la mejor configuración en el futuro a medida que esté disponible, pero aún no la he verificado porque aún no está disponible en mi GCC 9.2.1.
Aún tendrá que hacer una instanciación explícita para obtener la aceleración / guardar el disco, pero al menos tendremos una solución sensata para "Eliminar definiciones de los encabezados incluidos pero también exponer plantillas y una API externa" que no requiere copiar cosas alrededor de 100 veces.
El uso esperado (sin la iniciación explícita, no estoy seguro de cómo será la sintaxis exacta, consulte: ¿Cómo usar la instanciación explícita de la plantilla con módulos C ++ 20? )
helloworld.cpp
export module helloworld;
import <iostream>;
template<class T>
export void hello(T t) {
std::cout << t << std::end;
}
main.cpp
import helloworld;
int main() {
hello(1);
hello("world");
}
y luego la compilación mencionada en https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/
clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o
Entonces, a partir de esto, vemos que clang puede extraer la interfaz de la plantilla + implementación en la magia helloworld.pcm
, que debe contener alguna representación intermedia LLVM de la fuente: ¿Cómo se manejan las plantillas en el sistema de módulos C ++? lo que aún permite que suceda la especificación de la plantilla.
Cómo analizar rápidamente su compilación para ver si ganaría mucho con la creación de instancias de plantillas
Entonces, ¿tiene un proyecto complejo y desea decidir si la creación de instancias de plantilla traerá ganancias significativas sin hacer la refactorización completa?
El análisis a continuación puede ayudarlo a decidir, o al menos seleccionar los objetos más prometedores para refactorizar primero mientras experimenta, tomando prestadas algunas ideas de: Mi archivo de objeto C ++ es demasiado grande
# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
grep ' W ' > nm.log
# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log
# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log
# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log
# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list.
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
sort -k1 -n > nm.gains.log
# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log
# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log
El sueño: una caché de compilador de plantillas
Creo que la solución definitiva sería si pudiéramos construir con:
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
y luego myfile.o
reutilizaría automáticamente las plantillas compiladas previamente en todos los archivos.
Esto significaría 0 esfuerzo adicional para los programadores además de pasar esa opción CLI adicional a su sistema de compilación.
Una ventaja secundaria de la creación de instancias de plantilla explícita: los IDE de ayuda enumeran las instancias de plantilla
Descubrí que algunos IDE como Eclipse no pueden resolver "una lista de todas las instancias de plantilla utilizadas".
Por ejemplo, si está dentro de un código con plantilla y desea encontrar posibles valores de la plantilla, tendrá que encontrar los usos del constructor uno por uno y deducir los posibles tipos uno por uno.
Pero en Eclipse 2020-03 puedo enumerar fácilmente plantillas instanciadas explícitamente haciendo una búsqueda de Buscar todos los usos (Ctrl + Alt + G) en el nombre de la clase, que me señala, por ejemplo, desde:
template <class T>
struct AnimalTemplate {
T animal;
AnimalTemplate(T animal) : animal(animal) {}
std::string noise() {
return animal.noise();
}
};
a:
template class AnimalTemplate<Dog>;
Aquí hay una demostración: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Sin embargo, otra técnica de guerrila que podría usar fuera del IDE sería ejecutar nm -C
en el ejecutable final y hacer grep con el nombre de la plantilla:
nm -C main.out | grep AnimalTemplate
que apunta directamente al hecho de que Dog
fue una de las instancias:
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)