ฉันทำงานในโครงการ STAPL ซึ่งเป็นไลบรารี C ++ ที่มีเทมเพลตอย่างหนัก ในบางครั้งเราต้องทบทวนเทคนิคทั้งหมดเพื่อลดเวลาในการรวบรวม ที่นี่ฉันได้สรุปเทคนิคที่เราใช้ บางส่วนของเทคนิคเหล่านี้มีการระบุไว้ข้างต้น:
การค้นหาส่วนที่ใช้เวลานานที่สุด
แม้ว่าจะไม่มีความสัมพันธ์ที่พิสูจน์แล้วระหว่างความยาวสัญลักษณ์และเวลารวบรวม แต่เราสังเกตว่าขนาดสัญลักษณ์เฉลี่ยที่เล็กลงสามารถปรับปรุงเวลาการรวบรวมในคอมไพเลอร์ทั้งหมดได้ ดังนั้นเป้าหมายแรกของคุณคือค้นหาสัญลักษณ์ที่ใหญ่ที่สุดในรหัสของคุณ
วิธีที่ 1 - เรียงสัญลักษณ์ตามขนาด
คุณสามารถใช้nm
คำสั่งเพื่อแสดงสัญลักษณ์ตามขนาดของมัน:
nm --print-size --size-sort --radix=d YOUR_BINARY
ในคำสั่งนี้จะ--radix=d
ช่วยให้คุณเห็นขนาดเป็นตัวเลขทศนิยม (ค่าเริ่มต้นคือฐานสิบหก) ตอนนี้ด้วยการดูสัญลักษณ์ที่ใหญ่ที่สุดระบุว่าคุณสามารถแบ่งคลาสที่สอดคล้องกันและพยายามออกแบบใหม่โดยแยกชิ้นส่วนที่ไม่เทมเพลตในคลาสพื้นฐานหรือแบ่งคลาสออกเป็นหลายคลาส
วิธีที่ 2 - เรียงสัญลักษณ์ตามความยาว
คุณสามารถเรียกใช้ปกติnm
คำสั่งและท่อไปยังสคริปต์ที่คุณชื่นชอบ ( AWK , งูหลาม , ฯลฯ ) ในการจัดเรียงสัญลักษณ์ของพวกเขาขึ้นอยู่กับความยาว จากประสบการณ์ของเราวิธีนี้ระบุปัญหาที่ใหญ่ที่สุดในการทำให้ผู้สมัครดีกว่าวิธีที่ 1
วิธีที่ 3 - ใช้ Templight
" Templightเป็นเครื่องมือที่ใช้Clangเพื่อทำโปรไฟล์การใช้เวลาและหน่วยความจำของอินสแตนซ์ของแม่แบบอินสแตนซ์และเพื่อดำเนินการเซสชันการดีบักแบบอินเทอร์แอคทีฟ
คุณสามารถติดตั้ง Templight โดยตรวจสอบLLVMและ Clang ( คำแนะนำ ) และใช้ Templight patch บน การตั้งค่าเริ่มต้นสำหรับ LLVM และ Clang นั้นอยู่ในการดีบักและการยืนยันและสิ่งเหล่านี้อาจส่งผลต่อเวลาในการรวบรวมของคุณอย่างมาก ดูเหมือนว่า Templight ต้องการทั้งสองอย่างดังนั้นคุณต้องใช้การตั้งค่าเริ่มต้น กระบวนการติดตั้ง LLVM และ Clang ควรใช้เวลาประมาณหนึ่งชั่วโมง
หลังจากใช้ชุดข้อมูลแก้ไขคุณสามารถใช้ตำแหน่งtemplight++
ในโฟลเดอร์บิลด์ที่คุณระบุเมื่อทำการติดตั้งเพื่อคอมไพล์โค้ดของคุณ
ตรวจสอบให้แน่ใจว่าtemplight++
อยู่ในเส้นทางของคุณ ตอนนี้เพื่อคอมไพล์เพิ่มสวิตช์ต่อไปนี้ของคุณCXXFLAGS
ใน Makefile หรือตัวเลือกบรรทัดคำสั่งของคุณ:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
หรือ
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
หลังจากรวบรวมเสร็จแล้วคุณจะมี. trace.memory.pbf และ. trace.pbf สร้างขึ้นในโฟลเดอร์เดียวกัน หากต้องการเห็นภาพร่องรอยเหล่านี้คุณสามารถใช้เครื่องมือ Templightที่สามารถแปลงเป็นรูปแบบอื่นได้ ทำตามคำแนะนำเหล่านี้เพื่อติดตั้ง templight-conversion เรามักจะใช้การส่งออก callgrind คุณสามารถใช้เอาต์พุต GraphViz ได้หากโครงการของคุณมีขนาดเล็ก:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
ไฟล์ callgrind ที่สร้างขึ้นสามารถเปิดได้โดยใช้kcachegrindซึ่งคุณสามารถติดตามการสร้างอินสแตนซ์ที่ใช้เวลามากที่สุด / หน่วยความจำ
ลดจำนวนอินสแตนซ์ของแม่แบบอินสแตนซ์
แม้ว่าจะไม่มีวิธีแก้ไขปัญหาที่แน่นอนสำหรับการลดจำนวนอินสแตนซ์ของแม่แบบ แต่ก็มีแนวทางเล็กน้อยที่สามารถช่วยได้:
คลาส Refactor ที่มีอาร์กิวเมนต์เท็มเพลตมากกว่าหนึ่งข้อ
ตัวอย่างเช่นถ้าคุณมีชั้นเรียน
template <typename T, typename U>
struct foo { };
และทั้งสองT
และU
สามารถมี 10 ตัวเลือกที่แตกต่างกันคุณได้เพิ่มแม่แบบอินสแตนซ์ที่เป็นไปได้ของคลาสนี้เป็น 100 วิธีหนึ่งในการแก้ไขปัญหานี้คือการสรุปส่วนทั่วไปของรหัสเป็นคลาสอื่น วิธีอื่นคือการใช้การสืบทอดการสืบทอด (การย้อนกลับลำดับชั้นของคลาส) แต่ให้แน่ใจว่าเป้าหมายการออกแบบของคุณจะไม่ถูกบุกรุกก่อนที่จะใช้เทคนิคนี้
Refactor โค้ดที่ไม่ใช่เท็มเพลตให้กับแต่ละหน่วยการแปล
การใช้เทคนิคนี้คุณสามารถรวบรวมส่วนทั่วไปหนึ่งครั้งและเชื่อมโยงกับ TU อื่น ๆ ของคุณ (หน่วยการแปล) ในภายหลัง
ใช้อินสแตนซ์แม่แบบ extern (ตั้งแต่ C ++ 11)
หากคุณทราบอินสแตนซ์ที่เป็นไปได้ทั้งหมดของคลาสคุณสามารถใช้เทคนิคนี้เพื่อรวบรวมทุกกรณีในหน่วยการแปลที่แตกต่างกัน
ตัวอย่างเช่นใน:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
เรารู้ว่าคลาสนี้มีอินสแตนซ์ที่เป็นไปได้สามแบบ:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
ใส่ค่าข้างต้นในหน่วยการแปลและใช้คำสำคัญ extern ในไฟล์ส่วนหัวของคุณด้านล่างคำจำกัดความของคลาส:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
เทคนิคนี้ช่วยให้คุณประหยัดเวลาหากคุณรวบรวมการทดสอบที่แตกต่างกับชุดอินสแตนซ์ทั่วไป
หมายเหตุ: MPICH2 ละเว้นการสร้างอินสแตนซ์ที่ชัดเจน ณ จุดนี้และคอมไพล์คลาสที่สร้างอินสแตนซ์ในหน่วยการคอมไพล์ทั้งหมดเสมอ
ใช้ความสามัคคีสร้าง
แนวคิดทั้งหมดที่อยู่เบื้องหลังการสร้างความสามัคคีคือการรวมไฟล์. cc ทั้งหมดที่คุณใช้ในไฟล์เดียวและรวบรวมไฟล์นั้นเพียงครั้งเดียว การใช้วิธีนี้คุณสามารถหลีกเลี่ยงการคืนค่าส่วนทั่วไปของไฟล์ต่าง ๆ และหากโครงการของคุณมีไฟล์ทั่วไปจำนวนมากคุณอาจบันทึกการเข้าถึงดิสก์ด้วยเช่นกัน
ตัวอย่างเช่นสมมติว่าคุณมีสามไฟล์foo1.cc
, foo2.cc
, foo3.cc
และพวกเขาทั้งหมดรวมถึงtuple
จากSTL คุณสามารถสร้างสิ่งfoo-all.cc
ที่ดูเหมือน:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
คุณรวบรวมไฟล์นี้เพียงครั้งเดียวและอาจลดอินสแตนซ์ที่พบบ่อยในสามไฟล์ เป็นการยากที่จะทำนายว่าการปรับปรุงจะมีนัยสำคัญหรือไม่ แต่ความจริงข้อหนึ่งที่เห็นได้ชัดก็คือคุณจะสูญเสียความเท่าเทียมในงานสร้าง (คุณไม่สามารถรวบรวมสามไฟล์ในเวลาเดียวกันได้อีกต่อไป)
นอกจากนี้หากไฟล์ใด ๆ เหล่านี้มีหน่วยความจำจำนวนมากคุณอาจมีหน่วยความจำไม่เพียงพอก่อนที่การรวบรวมจะจบ ในคอมไพเลอร์บางตัวเช่นGCCนี่อาจเป็น ICE (Internal Compiler Error) คอมไพเลอร์ของคุณเนื่องจากไม่มีหน่วยความจำ ดังนั้นอย่าใช้เทคนิคนี้จนกว่าคุณจะรู้ข้อดีและข้อเสียทั้งหมด
ส่วนหัวที่คอมไพล์แล้ว
พรีคอมไพล์เฮดเดอร์ (PCHs) สามารถช่วยคุณประหยัดเวลาได้มากในการคอมไพล์โดยการคอมไพล์ไฟล์ส่วนหัวของคุณไปยังการแสดงระดับกลางที่คอมไพเลอร์รู้จัก ในการสร้างไฟล์ส่วนหัวที่คอมไพล์แล้วคุณจะต้องรวบรวมไฟล์ส่วนหัวของคุณด้วยคำสั่งการคอมไพล์ปกติของคุณ ตัวอย่างเช่นใน GCC:
$ g++ YOUR_HEADER.hpp
สิ่งนี้จะสร้างYOUR_HEADER.hpp.gch file
( .gch
เป็นส่วนขยายสำหรับไฟล์ PCH ใน GCC) ในโฟลเดอร์เดียวกัน ซึ่งหมายความว่าหากคุณรวมYOUR_HEADER.hpp
ไว้ในไฟล์อื่น ๆ คอมไพเลอร์จะใช้ของคุณYOUR_HEADER.hpp.gch
แทนYOUR_HEADER.hpp
ในโฟลเดอร์เดียวกันก่อน
มีสองประเด็นด้วยเทคนิคนี้:
- คุณต้องตรวจสอบให้แน่ใจว่าไฟล์ส่วนหัวที่ถูกคอมไพล์ล่วงหน้านั้นเสถียรและจะไม่เปลี่ยนแปลง ( คุณสามารถเปลี่ยน makefile ของคุณได้เสมอ )
- คุณสามารถรวมหนึ่ง PCH ต่อหนึ่งหน่วยการรวบรวมเท่านั้น (ในคอมไพเลอร์ส่วนใหญ่) ซึ่งหมายความว่าหากคุณมีไฟล์ส่วนหัวมากกว่าหนึ่งไฟล์ที่จะทำการคอมไพล์ล่วงหน้าคุณจะต้องรวมไว้ในไฟล์เดียว (เช่น
all-my-headers.hpp
) แต่นั่นหมายความว่าคุณต้องรวมไฟล์ใหม่ในทุกที่ โชคดีที่ GCC มีทางออกสำหรับปัญหานี้ ใช้-include
และให้ไฟล์ส่วนหัวใหม่ คุณสามารถใช้เครื่องหมายจุลภาคคั่นไฟล์ต่างกันได้โดยใช้เทคนิคนี้
ตัวอย่างเช่น:
g++ foo.cc -include all-my-headers.hpp
ใช้เนมสเปซที่ไม่มีชื่อหรือไม่ระบุชื่อ
เนมสเปซที่ไม่มีชื่อ ( เนมสเปซที่ไม่ระบุชื่อ) สามารถลดขนาดไบนารีที่สร้างขึ้นได้อย่างมาก เนมสเปซที่ไม่มีชื่อใช้การเชื่อมโยงภายในซึ่งหมายความว่าสัญลักษณ์ที่สร้างขึ้นในเนมสเปซเหล่านั้นจะไม่ปรากฏแก่ TU อื่น (หน่วยการแปลหรือการรวบรวม) คอมไพเลอร์จะสร้างชื่อเฉพาะสำหรับเนมสเปซที่ไม่มีชื่อ ซึ่งหมายความว่าหากคุณมีไฟล์ foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
และคุณได้รวมไฟล์นี้ในสอง TUs (ไฟล์. cc สองไฟล์และรวบรวมแยกต่างหาก) อินสแตนซ์ของแม่แบบ foo สองตัวจะไม่เหมือนกัน สิ่งนี้ละเมิดกฎข้อกำหนดหนึ่งข้อ (ODR) ด้วยเหตุผลเดียวกันการใช้เนมสเปซที่ไม่มีชื่อนั้นไม่ได้รับการสนับสนุนในไฟล์ส่วนหัว อย่าลังเลที่จะใช้มันใน.cc
ไฟล์ของคุณเพื่อหลีกเลี่ยงสัญลักษณ์ที่แสดงในไฟล์ไบนารีของคุณ ในบางกรณีการเปลี่ยนแปลงรายละเอียดภายในทั้งหมดสำหรับ.cc
ไฟล์แสดงให้เห็นถึงการลดลง 10% ในขนาดไบนารีที่สร้างขึ้น
การเปลี่ยนตัวเลือกการเปิดเผย
ในคอมไพเลอร์ที่ใหม่กว่าคุณสามารถเลือกสัญลักษณ์ของคุณเพื่อให้มองเห็นหรือมองไม่เห็นใน Dynamic Shared Objects (DSOs) เป็นการดีที่การเปลี่ยนการเปิดเผยสามารถปรับปรุงประสิทธิภาพของคอมไพเลอร์การเพิ่มประสิทธิภาพเวลาเชื่อมโยง (LTOs) และสร้างขนาดไบนารี ถ้าคุณดูไฟล์ส่วนหัว STL ใน GCC คุณจะเห็นว่ามันใช้กันอย่างแพร่หลาย ในการเปิดใช้งานตัวเลือกการเปิดเผยคุณต้องเปลี่ยนรหัสของคุณต่อฟังก์ชั่นต่อคลาสต่อตัวแปรและที่สำคัญกว่าต่อคอมไพเลอร์
ด้วยความช่วยเหลือของการมองเห็นคุณสามารถซ่อนสัญลักษณ์ที่คุณพิจารณาว่าเป็นเรื่องส่วนตัวจากวัตถุที่แชร์ที่สร้างขึ้น ใน GCC คุณสามารถควบคุมการมองเห็นสัญลักษณ์ได้โดยส่งค่าเริ่มต้นหรือซ่อนไว้ที่-visibility
ตัวเลือกคอมไพเลอร์ของคุณ นี่คือความรู้สึกบางอย่างคล้ายกับเนมสเปซที่ไม่มีชื่อ แต่ในทางที่ละเอียดและน่ารำคาญกว่า
หากคุณต้องการระบุความสามารถในการมองเห็นแต่ละกรณีคุณต้องเพิ่มคุณสมบัติต่อไปนี้ลงในฟังก์ชันตัวแปรและคลาสของคุณ:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
การเปิดเผยค่าเริ่มต้นใน GCC เป็นค่าเริ่มต้น (สาธารณะ) ซึ่งหมายความว่าหากคุณรวบรวมข้างต้นเป็นวิธีการใช้ไลบรารีร่วมกัน ( -shared
) foo2
และคลาสfoo3
จะไม่ปรากฏใน TU อื่น ๆ ( foo1
และfoo4
จะมองเห็นได้) หากคุณรวบรวม-visibility=hidden
แล้วเท่านั้นfoo1
จะสามารถมองเห็น แม้foo4
จะถูกซ่อนอยู่
คุณสามารถอ่านเพิ่มเติมเกี่ยวกับการมองเห็นในGCC วิกิพีเดีย