การสร้างอินสแตนซ์ที่ชัดเจนช่วยลดเวลาในการคอมไพล์และขนาดวัตถุ
นี่คือผลกำไรที่สำคัญที่สามารถให้ได้ พวกเขามาจากสองเอฟเฟกต์ต่อไปนี้ที่อธิบายโดยละเอียดในส่วนด้านล่าง:
- ลบคำจำกัดความออกจากส่วนหัวเพื่อป้องกันเครื่องมือสร้างจากการสร้างใหม่รวมถึง
- การกำหนดนิยามใหม่ของวัตถุ
ลบคำจำกัดความออกจากส่วนหัว
การสร้างอินสแตนซ์ที่ชัดเจนอนุญาตให้คุณทิ้งนิยามไว้ในไฟล์. cpp
เมื่อคำจำกัดความอยู่บนส่วนหัวและคุณแก้ไขระบบบิลด์อัจฉริยะจะคอมไพล์การรวมทั้งหมดอีกครั้งซึ่งอาจเป็นไฟล์ได้หลายสิบไฟล์ทำให้การคอมไพล์ช้ามาก
การใส่คำจำกัดความในไฟล์. 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
: ที่อยู่ภายในส่วน ศูนย์นี้เป็นเพราะเทมเพลตถูกใส่ลงในส่วนของตัวเองโดยอัตโนมัติ
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
เป็นชื่อที่สับสนMyTemplate<int>::f(int)>
ซึ่งc++filt
ตัดสินใจที่จะไม่เข้าใจผิด
ดังนั้นเราจึงเห็นว่ามีการสร้างส่วนแยกต่างหากสำหรับการสร้างอินสแตนซ์เมธอดเดียวและแต่ละส่วนใช้พื้นที่ในไฟล์อ็อบเจ็กต์แน่นอน
แนวทางแก้ไขปัญหาการกำหนดนิยามวัตถุใหม่
ปัญหานี้สามารถหลีกเลี่ยงได้โดยใช้การสร้างอินสแตนซ์ที่ชัดเจนและอย่างใดอย่างหนึ่ง:
ให้คำจำกัดความบน hpp และเพิ่มextern template
ใน hpp สำหรับประเภทที่กำลังจะถูกสร้างอินสแตนซ์อย่างชัดเจน
ตามที่อธิบายไว้ที่: การใช้เทมเพลตภายนอก (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
ดูเหมือนว่าคุณถูกบังคับให้เพิ่มการรวมไว้ในส่วนหัวการประกาศไปข้างหน้าไม่เพียงพอ: เทมเพลตภายนอกและประเภทที่ไม่สมบูรณ์ซึ่งจะเพิ่มการอ้างอิงส่วนหัว นิดหน่อย.
การย้ายคำจำกัดความบนไฟล์ 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
ในส่วนรวมทั้งหมด:
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
และที่อื่น ๆ ในฐานรหัส: รวมmytemplate_interface.hpp
ไม่ใช่mytemplate.hpp
-
mytemplate.hpp
: นิยามแม่แบบ
mytemplate_implementation.hpp
: รวมmytemplate.hpp
และเพิ่มextern
ให้กับทุกชั้นเรียนที่จะสร้างอินสแตนซ์
mytemplate.cpp
: รวมmytemplate.hpp
และสร้างอินสแตนซ์ที่ชัดเจน
main.cpp
และที่อื่น ๆ ในฐานรหัส: รวมmytemplate_implementation.hpp
ไม่ใช่mytemplate.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
คุณยังคงต้องทำการสร้างอินสแตนซ์อย่างชัดเจนเพื่อให้ได้ speedup / disk saving แต่อย่างน้อยเราก็มีวิธีแก้ปัญหาสำหรับ "ลบคำจำกัดความจากส่วนหัวที่รวมอยู่ แต่ยังเปิดเผยเทมเพลตเป็น API ภายนอก" ซึ่งไม่จำเป็นต้องคัดลอกสิ่งต่างๆประมาณ 100 ครั้ง
การใช้งานที่คาดหวัง (โดยไม่มีการสอดแทรกอย่างชัดเจนไม่แน่ใจว่าไวยากรณ์จะเป็นอย่างไรโปรดดูที่: วิธีใช้การสร้างอินสแตนซ์แบบชัดแจ้งของเทมเพลตกับโมดูล 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: เทมเพลตถูกจัดการอย่างไรในระบบโมดูล C ++ ซึ่งยังคงทำให้ข้อกำหนดของเทมเพลตเกิดขึ้นได้
วิธีวิเคราะห์บิลด์ของคุณอย่างรวดเร็วเพื่อดูว่าจะได้รับประโยชน์มากมายจากการสร้างอินสแตนซ์เทมเพลตหรือไม่
คุณมีโครงการที่ซับซ้อนและต้องการตัดสินใจว่าการสร้างอินสแตนซ์เทมเพลตจะนำมาซึ่งผลกำไรอย่างมีนัยสำคัญโดยไม่ต้องทำ Refactor จริงหรือไม่?
การวิเคราะห์ด้านล่างอาจช่วยให้คุณตัดสินใจได้หรืออย่างน้อยก็เลือกวัตถุที่มีแนวโน้มมากที่สุดเพื่อ refactor ก่อนในขณะที่คุณทำการทดลองโดยยืมแนวคิดจาก: ไฟล์อ็อบเจ็กต์ 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
จะใช้เทมเพลตที่คอมไพล์ก่อนหน้านี้ซ้ำกับไฟล์ต่างๆโดยอัตโนมัติ
นี่หมายถึงความพยายามพิเศษ 0 สำหรับโปรแกรมเมอร์นอกเหนือจากการส่งตัวเลือก CLI พิเศษนั้นไปยังระบบบิลด์ของคุณ
โบนัสรองของการสร้างอินสแตนซ์เทมเพลตที่ชัดเจน: ช่วย IDEs การสร้างอินสแตนซ์เทมเพลตรายการ
ฉันพบว่า IDE บางตัวเช่น Eclipse ไม่สามารถแก้ไข "รายการของอินสแตนซ์เทมเพลตทั้งหมดที่ใช้"
ตัวอย่างเช่นหากคุณอยู่ในโค้ดเทมเพลตและคุณต้องการค้นหาค่าที่เป็นไปได้ของเทมเพลตคุณจะต้องค้นหาตัวสร้างใช้ทีละรายการและสรุปประเภทที่เป็นไปได้ทีละประเภท
แต่ใน 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)