명시 적 인스턴스화를 통해 컴파일 시간과 개체 크기를 줄일 수 있습니다.
이것이 제공 할 수있는 주요 이점입니다. 아래 섹션에서 자세히 설명하는 다음 두 가지 효과에서 비롯됩니다.
- 빌드 도구가 includer를 다시 빌드하지 못하도록 헤더에서 정의를 제거합니다.
- 객체 재정의
헤더에서 정의 제거
명시 적 인스턴스화를 사용하면 .cpp 파일에 정의를 남길 수 있습니다.
정의가 헤더에 있고이를 수정하면 지능형 빌드 시스템이 모든 includer를 재 컴파일합니다. 이는 수십 개의 파일이 될 수 있으므로 컴파일 속도가 느려집니다.
.cpp 파일에 정의를 넣는 것은 외부 라이브러리가 자신의 새 클래스와 함께 템플릿을 재사용 할 수 없다는 단점이 있지만 아래의 "포함 된 헤더에서 정의를 제거하고 템플릿을 외부 API에 노출"은 해결 방법을 보여줍니다.
아래의 구체적인 예를 참조하십시오.
객체 재정의 이득 : 문제 이해
헤더 파일에 템플릿을 완전히 정의하는 경우 해당 헤더를 포함하는 모든 단일 컴파일 단위는 모든 다른 템플릿 인수 사용에 대해 템플릿의 암시 적 복사본을 컴파일합니다.
이것은 쓸모없는 디스크 사용량과 컴파일 시간을 의미합니다.
다음은 해당 파일에서의 사용으로 인해 main.cpp
및 notmain.cpp
암시 적으로 정의 하는 구체적인 예 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); }
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 업스트림 .
다음을 사용하여 심볼 컴파일 및보기 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
산출:
notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
에서 man nm
우리 W
는 이것이 템플릿 함수이기 때문에 GCC가 선택한 약한 기호 를 의미합니다. 약한 기호는에 대한 컴파일 된 암시 적으로 생성 된 코드 MyTemplate<int>
가 두 파일 모두 에서 컴파일되었음을 의미 합니다.
여러 정의로 링크 타임에 폭발하지 않는 이유 는 링커가 여러 약한 정의를 허용하고 그중 하나를 선택하여 최종 실행 파일에 넣기 때문입니다.
출력의 숫자는 다음을 의미합니다.
0000000000000000
: 섹션 내 주소. 이 0은 템플릿이 자동으로 자체 섹션에 배치되기 때문입니다.
0000000000000017
: 그들을 위해 생성 된 코드의 크기
우리는 이것을 좀 더 명확하게 볼 수 있습니다 :
objdump -S main.o | c++filt
다음으로 끝나는 :
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
과 _ZN10MyTemplateIiE1fEi
의 변환 된 이름입니다 unmangle하지 않기로 결정했다.MyTemplate<int>::f(int)>
c++filt
따라서 모든 단일 메소드 인스턴스화에 대해 별도의 섹션이 생성되고 각 섹션이 개체 파일의 공간을 차지한다는 것을 알 수 있습니다.
객체 재정의 문제에 대한 솔루션
이 문제는 명시 적 인스턴스화 및 다음 중 하나를 사용하여 방지 할 수 있습니다.
hpp에 정의를 유지하고 추가 extern template
명시 적으로 인스턴스화 할 유형에 대해 hpp를 .
설명 된대로 : extern 템플릿 (C ++ 11)을 사용 extern template
하면 명시 적 인스턴스화를 제외하고 완전히 정의 된 템플릿이 컴파일 단위에 의해 인스턴스화되는 것을 방지합니다. 이렇게하면 명시 적 인스턴스화 만 최종 개체에 정의됩니다.
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); }
단점 :
- 헤더 전용 라이브러리 인 경우 외부 프로젝트가 자신의 명시 적 인스턴스화를 수행하도록 강제합니다. 헤더 전용 라이브러리가 아닌 경우이 솔루션이 가장 좋습니다.
- 템플릿 유형이 자체 프로젝트에 정의되어 있고 기본 제공되지 않는
int
경우 헤더에 포함을 추가해야하는 것 같습니다. 포워드 선언만으로는 충분하지 않습니다. extern 템플릿 및 불완전한 유형 이로 인해 헤더 종속성이 증가합니다. 약간.
cpp 파일에서 정의를 이동하고 hpp에 선언 만 남겨 둡니다. 즉, 원래 예제를 다음과 같이 수정합니다.
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>;
단점 : 외부 프로젝트는 자체 유형으로 템플릿을 사용할 수 없습니다. 또한 모든 유형을 명시 적으로 인스턴스화해야합니다. 그러나 프로그래머가 잊지 못할 이후로 이것은 좋은 점일 것입니다.
hpp에 정의를 유지 extern template
하고 모든 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); }
단점 : 모든 포함자는 extern
CPP 파일에 를 추가해야 하므로 프로그래머가 잊을 가능성이 높습니다.
이러한 솔루션 중 하나를 사용하면 nm
이제 다음이 포함됩니다.
notmain.o
U MyTemplate<int>::f(int)
main.o
U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)
우리가 볼 수 있도록 단지 mytemplate.o
의 편집이 MyTemplate<int>
있지만, 원하는대로를 notmain.o
하고 main.o
있기 때문에하지 않는 U
수단이 정의되지 않은.
포함 된 헤더에서 정의를 제거하고 헤더 전용 라이브러리의 외부 API에 템플릿을 노출합니다.
라이브러리가 헤더 전용이 아닌 경우 extern template
프로젝트를 사용하면 명시 적 템플릿 인스턴스화의 개체를 포함하는 개체 파일에 연결되기 때문에 메서드가 작동합니다.
그러나 헤더 전용 라이브러리의 경우 둘 다 원하는 경우 :
- 프로젝트 컴파일 속도 향상
- 다른 사람이 사용할 수 있도록 헤더를 외부 라이브러리 API로 노출
다음 중 하나를 시도 할 수 있습니다.
-
mytemplate.hpp
: 템플릿 정의
mytemplate_interface.hpp
:의 정의와 만 일치하는 템플릿 선언, 정의 mytemplate_interface.hpp
없음
mytemplate.cpp
: mytemplate.hpp
명시 적 인스턴스를 포함 하고 만듭니다.
main.cpp
그리고 코드베이스의 다른 모든 곳 : include mytemplate_interface.hpp
, notmytemplate.hpp
-
mytemplate.hpp
: 템플릿 정의
mytemplate_implementation.hpp
: 인스턴스화 될 모든 클래스를 포함 mytemplate.hpp
하고 추가 extern
합니다.
mytemplate.cpp
: mytemplate.hpp
명시 적 인스턴스를 포함 하고 만듭니다.
main.cpp
그리고 코드베이스의 다른 모든 곳 : include mytemplate_implementation.hpp
, notmytemplate.hpp
또는 여러 헤더의 경우 더 좋을 수 있습니다. 폴더 안에 intf
/ impl
폴더를 만들고 항상 이름으로 includes/
사용 하십시오 mytemplate.hpp
.
mytemplate_interface.hpp
방법은 다음과 같습니다 :
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;
}
컴파일 및 실행 :
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
산출:
2
Ubuntu 18.04에서 테스트되었습니다.
C ++ 20 모듈
https://en.cppreference.com/w/cpp/language/modules
이 기능이 사용 가능 해지면 앞으로 최상의 설정을 제공 할 것이라고 생각하지만 아직 GCC 9.2.1에서 사용할 수 없기 때문에 아직 확인하지 않았습니다.
속도 향상 / 디스크 절약을 얻으려면 명시 적 인스턴스화를 수행해야하지만 적어도 100 번 정도 복사 할 필요가없는 "포함 된 헤더에서 정의를 제거하고 템플릿을 외부 API에 노출"에 대한 정상적인 솔루션이 있습니다.
예상되는 사용법 (명시적인 설명없이, 정확한 구문이 무엇인지 확실하지 않음, 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");
}
그런 다음 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
따라서 여기에서 clang은 helloworld.pcm
소스의 LLVM 중간 표현을 포함해야하는 magic으로 템플릿 인터페이스 + 구현을 추출 할 수 있음을 알 수 있습니다. 템플릿 은 C ++ 모듈 시스템에서 어떻게 처리됩니까? 여전히 템플릿 사양이 발생할 수 있습니다.
빌드를 빠르게 분석하여 템플릿 인스턴스화에서 많은 이점을 얻을 수 있는지 확인하는 방법
그렇다면 복잡한 프로젝트가 있고 템플릿 인스턴스화가 실제로 전체 리팩터링을 수행하지 않고도 상당한 이득을 가져올 지 결정하고 싶습니까?
아래 분석은 다음에서 몇 가지 아이디어를 빌려 실험하는 동안 가장 먼저 리팩토링 할 가장 유망한 객체를 결정하거나 적어도 선택하는 데 도움이 될 수 있습니다. 내 C ++ 객체 파일이 너무 큽니다.
# 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
꿈 : 템플릿 컴파일러 캐시
궁극적 인 해결책은 다음과 같이 구축 할 수있는 것입니다.
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
그런 다음 myfile.o
파일간에 이전에 컴파일 된 템플릿을 자동으로 재사용합니다.
이는 추가 CLI 옵션을 빌드 시스템에 전달하는 것 외에 프로그래머의 추가 노력이 없음을 의미합니다.
명시적인 템플릿 인스턴스화의 두 번째 보너스 : IDE가 템플릿 인스턴스화를 나열하는 데 도움이됩니다.
Eclipse와 같은 일부 IDE는 "사용 된 모든 템플릿 인스턴스화 목록"을 해결할 수 없음을 발견했습니다.
예를 들어, 템플릿 코드 내부에 있고 템플릿의 가능한 값을 찾으려면 생성자 사용법을 하나씩 찾아 가능한 유형을 하나씩 추론해야합니다.
그러나 Eclipse 2020-03에서는 클래스 이름에 대해 Find all usages (Ctrl + Alt + G) 검색을 수행하여 명시 적으로 인스턴스화 된 템플릿을 쉽게 나열 할 수 있습니다.
template <class T>
struct AnimalTemplate {
T animal;
AnimalTemplate(T animal) : animal(animal) {}
std::string noise() {
return animal.noise();
}
};
에:
template class AnimalTemplate<Dog>;
데모 : https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
IDE 외부에서 사용할 수있는 또 다른 게릴라 기술 nm -C
은 최종 실행 파일에서 실행하고 템플릿 이름을 grep하는 것입니다.
nm -C main.out | grep AnimalTemplate
Dog
인스턴스화 중 하나 라는 사실을 직접 가리 킵니다 .
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)