A instanciação explícita permite reduzir os tempos de compilação e os tamanhos dos objetos
Esses são os principais ganhos que ela pode proporcionar. Eles vêm dos dois efeitos a seguir descritos em detalhes nas seções abaixo:
- remova as definições dos cabeçalhos para evitar que as ferramentas de construção reconstruam os includers (economiza tempo)
- redefinição de objeto (economiza tempo e tamanho)
Remova as definições dos cabeçalhos
A instanciação explícita permite que você deixe definições no arquivo .cpp.
Quando a definição está no cabeçalho e você a modifica, um sistema de compilação inteligente recompila todos os includers, que podem ser dezenas de arquivos, possivelmente tornando a recompilação incremental após uma única alteração de arquivo insuportavelmente lenta.
Colocar definições em arquivos .cpp tem a desvantagem de que as bibliotecas externas não podem reutilizar o modelo com suas próprias novas classes, mas "Remova as definições dos cabeçalhos incluídos, mas também exponha os modelos de uma API externa" abaixo mostra uma solução alternativa.
Veja exemplos concretos abaixo.
Ganhos de redefinição de objeto: entendendo o problema
Se você apenas definir completamente um modelo em um arquivo de cabeçalho, cada unidade de compilação que inclui esse cabeçalho termina compilando sua própria cópia implícita do modelo para cada uso de argumento de modelo diferente feito.
Isso significa muito uso de disco e tempo de compilação inúteis.
Aqui está um exemplo concreto, em que ambos main.cpp
e notmain.cpp
implicitamente definem MyTemplate<int>
devido ao seu uso nesses arquivos.
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 upstream .
Compile e veja os símbolos com 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
Resultado:
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 fraco, que o GCC escolheu porque esta é uma função de modelo.
O motivo pelo qual ele não explode no momento do link com várias definições é que o vinculador aceita várias definições fracas e apenas escolhe uma delas para colocar no executável final, e todas são iguais em nosso caso, então tudo é bem.
Os números na saída significam:
0000000000000000
: endereço dentro da seção. Esse zero ocorre porque os modelos são colocados automaticamente em suas próprias seções
0000000000000017
: tamanho do código gerado para eles
Podemos ver isso um pouco mais claramente com:
objdump -S main.o | c++filt
que termina em:
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
e _ZN10MyTemplateIiE1fEi
é o nome mutilado do MyTemplate<int>::f(int)>
qual c++filt
decidiu não desfazer.
Portanto, vemos que uma seção separada é gerada para cada instanciação de método e que cada um deles ocupa, é claro, espaço nos arquivos de objeto.
Soluções para o problema de redefinição de objeto
Esse problema pode ser evitado usando instanciação explícita e:
mantenha a definição em hpp e adicione extern template
hpp para tipos que serão explicitamente instanciados.
Conforme explicado em: usar extern template (C ++ 11) extern template
evita que um template completamente definido seja instanciado por unidades de compilação, exceto para nossa instanciação explícita. Dessa forma, apenas nossa instanciação explícita será definida nos objetos finais:
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); }
Desvantagens:
- a definição permanece no cabeçalho, tornando a recompilação de alteração de arquivo único para esse cabeçalho possivelmente lenta
- se você for uma biblioteca apenas de cabeçalho, você força os projetos externos a fazerem sua própria instanciação explícita. Se você não for uma biblioteca apenas de cabeçalho, esta solução é provavelmente a melhor.
- se o tipo de modelo é definido em seu próprio projeto e não em um embutido
int
, parece que você é forçado a adicionar a inclusão para ele no cabeçalho, uma declaração direta não é suficiente: modelo externo e tipos incompletos Isso aumenta as dependências do cabeçalho um pouco.
movendo a definição no arquivo cpp, deixe apenas a declaração no hpp, ou seja, modifique o exemplo original para ser:
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>;
Desvantagem: projetos externos não podem usar seu modelo com seus próprios tipos. Além disso, você é forçado a instanciar explicitamente todos os tipos. Mas talvez isso seja uma vantagem, já que os programadores não esquecerão.
mantenha a definição no hpp e adicione extern template
em todos os incluídos:
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); }
Desvantagem: todos os incluídos precisam adicionar o extern
aos seus arquivos CPP, o que os programadores provavelmente se esquecerão de fazer.
Com qualquer uma dessas soluções, nm
agora contém:
notmain.o
U MyTemplate<int>::f(int)
main.o
U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)
então vemos que tem apenas mytemplate.o
uma compilação de MyTemplate<int>
como desejado, enquanto notmain.o
e main.o
não porque U
significa indefinido.
Remova as definições dos cabeçalhos incluídos, mas também exponha os modelos de uma API externa em uma biblioteca somente de cabeçalho
Se a sua biblioteca não for apenas cabeçalho, o extern template
método funcionará, já que o uso de projetos irá apenas vincular ao seu arquivo objeto, que conterá o objeto da instanciação explícita do template.
No entanto, para bibliotecas apenas de cabeçalho, se você quiser:
- acelerar a compilação do seu projeto
- expor cabeçalhos como uma API de biblioteca externa para outros usarem
então você pode tentar um dos seguintes:
-
mytemplate.hpp
: definição de modelo
mytemplate_interface.hpp
: declaração do modelo apenas correspondendo às definições de mytemplate_interface.hpp
, sem definições
mytemplate.cpp
: inclui mytemplate.hpp
e faz instanciações explícitas
main.cpp
e em qualquer outro lugar na base de código: include mytemplate_interface.hpp
, notmytemplate.hpp
-
mytemplate.hpp
: definição de modelo
mytemplate_implementation.hpp
: inclui mytemplate.hpp
e adiciona extern
a cada classe que será instanciada
mytemplate.cpp
: inclui mytemplate.hpp
e faz instanciações explícitas
main.cpp
e em qualquer outro lugar na base de código: include mytemplate_implementation.hpp
, notmytemplate.hpp
Ou melhor ainda, para cabeçalhos múltiplos: crie uma pasta intf
/ impl
dentro de sua includes/
pasta e use mytemplate.hpp
sempre como o nome.
A mytemplate_interface.hpp
abordagem é assim:
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;
}
Compile e execute:
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
Resultado:
2
Testado no Ubuntu 18.04.
Módulos C ++ 20
https://en.cppreference.com/w/cpp/language/modules
Acho que esse recurso fornecerá a melhor configuração no futuro, à medida que estiver disponível, mas ainda não o verifiquei porque ainda não está disponível em meu GCC 9.2.1.
Você ainda terá que fazer uma instanciação explícita para obter a aceleração / economia de disco, mas pelo menos teremos uma solução sensata para "Remover definições de cabeçalhos incluídos, mas também expor modelos de API externa" que não exige a cópia de cerca de 100 vezes.
O uso esperado (sem o insantiation explícito, não tenho certeza de como será a sintaxe exata, consulte: Como usar a instanciação explícita de modelo com módulos C ++ 20? ) Seja algo ao longo:
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");
}
e a compilação mencionada em 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
Portanto, vemos que o clang pode extrair a interface + implementação do template na mágica helloworld.pcm
, que deve conter alguma representação LLVM intermediária da fonte: Como os templates são tratados no sistema de módulo C ++? que ainda permite que a especificação do modelo aconteça.
Como analisar rapidamente sua construção para ver se ela ganharia muito com a instanciação do modelo
Então, você tem um projeto complexo e quer decidir se a instanciação do template trará ganhos significativos sem realmente fazer a refatoração completa?
A análise abaixo pode ajudá-lo a decidir, ou pelo menos selecionar os objetos mais promissores para refatorar primeiro enquanto você experimenta, pegando emprestadas algumas idéias de: Meu arquivo de objeto C ++ é muito 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
O sonho: um cache de compilador de template
Acho que a solução final seria se pudéssemos construir com:
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
e então myfile.o
reutilizaria automaticamente os modelos previamente compilados nos arquivos.
Isso significaria 0 esforço extra para os programadores, além de passar essa opção CLI extra para seu sistema de construção.
Um bônus secundário de instanciação de modelo explícito: ajuda IDEs listar instanciações de modelo
Descobri que alguns IDEs como o Eclipse não podem resolver "uma lista de todas as instanciações de modelo usadas".
Então, por exemplo, se você estiver dentro de um código de modelo e quiser encontrar os valores possíveis do modelo, terá que encontrar os usos do construtor um por um e deduzir os tipos possíveis um por um.
Mas no Eclipse 2020-03, posso facilmente listar modelos explicitamente instanciados fazendo uma pesquisa Localizar todos os usos (Ctrl + Alt + G) no nome da classe, que me aponta, por exemplo:
template <class T>
struct AnimalTemplate {
T animal;
AnimalTemplate(T animal) : animal(animal) {}
std::string noise() {
return animal.noise();
}
};
para:
template class AnimalTemplate<Dog>;
Aqui está uma demonstração: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Outra técnica de guerrilha que você pode usar fora do IDE, no entanto, seria executar nm -C
no executável final e executar o grep no nome do modelo:
nm -C main.out | grep AnimalTemplate
que aponta diretamente para o fato de que Dog
foi uma das instanciações:
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)