ฉันต้องการที่จะกระโดดเข้าไปที่นี่ท่ามกลางคำตอบที่ยอดเยี่ยมแล้วและยอมรับว่าฉันได้ใช้วิธีที่น่าเกลียดของการทำงานย้อนกลับไปยังรูปแบบการต่อต้านการเปลี่ยนรหัส polymorphic เป็นswitches
หรือif/else
กิ่งไม้ด้วยกำไรที่วัดได้ แต่ฉันไม่ได้ทำขายส่งนี้เฉพาะเส้นทางที่สำคัญที่สุด ไม่จำเป็นต้องเป็นขาวดำ
ในฐานะที่เป็นข้อจำกัดความรับผิดชอบฉันทำงานในพื้นที่เช่น raytracing ที่ความถูกต้องไม่ยากที่จะบรรลุ (และมักจะคลุมเครือและประมาณต่อไป) ในขณะที่ความเร็วมักเป็นคุณสมบัติที่แข่งขันกันมากที่สุด การลดเวลาในการเรนเดอร์มักจะเป็นหนึ่งในคำขอของผู้ใช้ที่พบบ่อยที่สุดโดยเราเกาหัวของเราอย่างต่อเนื่องและหาวิธีที่จะทำให้สำเร็จสำหรับเส้นทางที่สำคัญที่สุดที่วัดได้
การปรับสภาพโพลีมอร์ฟิคแบบมีเงื่อนไข
ครั้งแรกก็เข้าใจว่าทำไมมูลค่าแตกต่างสามารถเป็นที่นิยมจากด้านการบำรุงรักษากว่ากำหนดการทำงานตามเงื่อนไข ( switch
หรือพวงของif/else
งบ) ประโยชน์หลักที่นี่คือการขยาย
ด้วยรหัส polymorphic เราสามารถแนะนำ subtype ใหม่ให้กับ codebase ของเราเพิ่มอินสแตนซ์ของมันให้กับโครงสร้างข้อมูล polymorphic บางส่วนและมีรหัส polymorphic ที่มีอยู่ทั้งหมดยังคงทำงานโดยอัตโนมัติโดยไม่มีการแก้ไขเพิ่มเติม หากคุณมีรหัสกระจัดกระจายไปทั่ว codebase ขนาดใหญ่ที่มีลักษณะคล้ายกับรูปแบบของ"ถ้ารูปแบบนี้เป็น" foo "ให้ทำเช่นนั้น"คุณอาจพบว่าตัวเองมีภาระที่น่ากลัวในการอัพเดท 50 ส่วนที่แตกต่างกันของรหัส รูปแบบใหม่ของสิ่งและยังคงหายไปไม่กี่
ประโยชน์ด้านการบำรุงรักษาของ polymorphism ลดน้อยลงหากคุณมีสองสามส่วนหรือแม้แต่ส่วนหนึ่งของ codebase ของคุณที่ต้องทำการตรวจสอบประเภทดังกล่าว
อุปสรรคการเพิ่มประสิทธิภาพ
ฉันขอแนะนำไม่ให้มองสิ่งนี้จากมุมมองของการแตกแขนงและการวางท่อมากนักและดูเพิ่มเติมจากแนวคิดการออกแบบคอมไพเลอร์ของอุปสรรคในการปรับให้เหมาะสม มีวิธีการปรับปรุงการคาดคะเนสาขาที่ใช้กับทั้งสองกรณีเช่นการเรียงลำดับข้อมูลตามประเภทย่อย (ถ้าเหมาะกับลำดับ)
สิ่งที่แตกต่างกันระหว่างสองกลยุทธ์นี้คือปริมาณข้อมูลที่เครื่องมือเพิ่มประสิทธิภาพมีไว้ล่วงหน้า การเรียกใช้ฟังก์ชันที่เป็นที่รู้จักนั้นให้ข้อมูลมากขึ้นการเรียกใช้ฟังก์ชันทางอ้อมซึ่งเรียกฟังก์ชันที่ไม่รู้จัก ณ เวลารวบรวมทำให้เกิดอุปสรรคในการปรับให้เหมาะสม
เมื่อฟังก์ชั่นที่ถูกเรียกใช้นั้นคอมไพเลอร์สามารถลบล้างโครงสร้างและสควอชมันลงไปที่ smithereens, การโทรแบบอินไลน์, การกำจัดค่า aliasing ที่อาจเกิดขึ้น, ทำงานได้ดีกว่าในการจัดสรรคำแนะนำ / การลงทะเบียน LUT ขนาดเล็กแบบเข้ารหัสเมื่อเหมาะสม (บางสิ่งบางอย่าง GCC 5.3 เพิ่งทำให้ฉันประหลาดใจด้วยswitch
คำสั่งโดยใช้ LUT แบบกำหนดรหัสแบบยากของข้อมูลสำหรับผลลัพธ์แทนที่จะเป็นตารางกระโดด)
ผลประโยชน์เหล่านั้นบางส่วนหายไปเมื่อเราเริ่มแนะนำการรวบรวมเวลาที่ไม่ทราบในการผสมเช่นเดียวกับกรณีของการเรียกใช้ฟังก์ชันทางอ้อมและนั่นคือสิ่งที่การแตกสาขาแบบมีเงื่อนไขสามารถให้ความได้เปรียบที่สุด
การเพิ่มประสิทธิภาพหน่วยความจำ
ลองดูตัวอย่างวิดีโอเกมที่ประกอบด้วยการประมวลผลลำดับของสิ่งมีชีวิตซ้ำ ๆ กันในวงที่แน่น ในกรณีเช่นนี้เราอาจมีคอนเทนเนอร์ polymorphic ดังนี้:
vector<Creature*> creatures;
หมายเหตุ: เพื่อความเรียบง่ายฉันหลีกเลี่ยงunique_ptr
ที่นี่
... ซึ่งCreature
เป็นประเภทพื้นฐาน polymorphic ในกรณีนี้ปัญหาอย่างหนึ่งของภาชนะบรรจุ polymorphic คือพวกเขามักต้องการจัดสรรหน่วยความจำสำหรับแต่ละชนิดย่อยแยกกัน / แยกกัน (เช่น: ใช้การขว้างเป็นค่าเริ่มต้นoperator new
สำหรับสิ่งมีชีวิตแต่ละตัว)
ซึ่งมักจะให้ความสำคัญเป็นอันดับแรกสำหรับการปรับให้เหมาะสม (เราจำเป็นต้องใช้) - อิงตามหน่วยความจำมากกว่าการแยกสาขา กลยุทธ์หนึ่งที่นี่คือการใช้ตัวจัดสรรแบบคงที่สำหรับแต่ละประเภทย่อยส่งเสริมการแสดงที่ต่อเนื่องกันโดยการจัดสรรในหน่วยย่อยขนาดใหญ่และการรวมหน่วยความจำสำหรับแต่ละประเภทย่อยที่ถูกจัดสรร ด้วยกลยุทธ์ดังกล่าวมันสามารถช่วยจัดเรียงcreatures
ตู้คอนเทนเนอร์ตามประเภทย่อย (เช่นเดียวกับที่อยู่) ได้อย่างแน่นอนเนื่องจากไม่เพียง แต่จะช่วยปรับปรุงการพยากรณ์สาขา แต่ยังปรับปรุงการอ้างอิงในท้องที่ด้วย จากบรรทัดแคชเดียวก่อนจะถูกไล่ออก)
การแบ่งส่วนข้อมูลเสมือนจริงของโครงสร้างข้อมูลและลูปบางส่วน
สมมติว่าคุณผ่านการเคลื่อนไหวเหล่านี้และคุณยังคงต้องการความเร็วเพิ่มขึ้น เป็นเรื่องที่น่าสังเกตว่าแต่ละขั้นตอนที่เราเข้าไปเกี่ยวข้องที่นี่เป็นการบำรุงรักษาที่ลดระดับลงและเราจะอยู่ในขั้นตอนการขัดโลหะบ้างแล้วพร้อมกับลดประสิทธิภาพลง ดังนั้นจึงจำเป็นต้องมีความต้องการด้านประสิทธิภาพที่สำคัญหากเราก้าวเข้าสู่ดินแดนนี้ซึ่งเรายินดีที่จะเสียสละความสามารถในการบำรุงรักษาให้ดียิ่งขึ้นเพื่อให้ได้ประสิทธิภาพที่เล็กลงและเล็กลง
แต่ขั้นตอนต่อไปที่จะลอง (และพร้อมเสมอที่จะสำรองการเปลี่ยนแปลงของเราหากไม่ได้ช่วยเลย) อาจจะเป็นการพัฒนาระบบเสมือนจริงแบบแมนนวล
เคล็ดลับการควบคุมเวอร์ชัน: หากคุณไม่เข้าใจการเพิ่มประสิทธิภาพมากกว่าฉันมันก็คุ้มค่าที่จะสร้างสาขาใหม่ ณ จุดนี้ด้วยความเต็มใจที่จะโยนมันออกไปหากความพยายามเพิ่มประสิทธิภาพของเราพลาดซึ่งอาจเกิดขึ้นได้เป็นอย่างดี สำหรับฉันมันคือการทดลองและข้อผิดพลาดหลังจากคะแนนประเภทนี้ถึงแม้จะมี profiler อยู่ในมือ
อย่างไรก็ตามเราไม่จำเป็นต้องใช้ความคิดขายส่งนี้ ต่อจากตัวอย่างของเราสมมติว่าวิดีโอเกมนี้ประกอบไปด้วยสิ่งมีชีวิตส่วนใหญ่ของมนุษย์ ในกรณีเช่นนี้เราสามารถสร้างสิ่งมีชีวิตมนุษย์เพียงอย่างเดียวโดยยกพวกมันออกมาและสร้างโครงสร้างข้อมูลแยกต่างหากสำหรับพวกเขา
vector<Human> humans; // common case
vector<Creature*> other_creatures; // additional rare-case creatures
นี่ก็หมายความว่าทุกพื้นที่ใน codebase ของเราที่ต้องการประมวลผลสิ่งมีชีวิตจำเป็นต้องมีห่วงกรณีพิเศษแยกต่างหากสำหรับสิ่งมีชีวิตมนุษย์ แต่นั่นก็เป็นการกำจัดค่าใช้จ่ายในการจัดส่งแบบไดนามิก (หรืออาจจะเป็นอุปสรรคการเพิ่มประสิทธิภาพที่เหมาะสม) สำหรับมนุษย์ซึ่งเป็นประเภทสิ่งมีชีวิตที่พบได้บ่อยที่สุด หากพื้นที่เหล่านี้มีจำนวนมากและเราสามารถจ่ายได้เราอาจทำสิ่งนี้:
vector<Human> humans; // common case
vector<Creature*> other_creatures; // additional rare-case creatures
vector<Creature*> creatures; // contains humans and other creatures
... หากเราสามารถจ่ายได้เส้นทางที่มีความสำคัญน้อยกว่าสามารถดำรงอยู่ได้เหมือนเดิมและดำเนินการสิ่งมีชีวิตทุกประเภทอย่างเป็นนามธรรม เส้นทางที่สำคัญสามารถดำเนินการhumans
ในหนึ่งวงและother_creatures
ในวงที่สอง
เราสามารถขยายกลยุทธ์นี้ได้ตามต้องการและอาจทำให้บางคนได้รับวิธีนี้ แต่มันก็คุ้มค่าที่จะสังเกตว่าเรากำลังลดระดับการบำรุงรักษาในกระบวนการลงมากน้อยเพียงใด การใช้เทมเพลตฟังก์ชั่นที่นี่สามารถช่วยสร้างรหัสสำหรับทั้งมนุษย์และสิ่งมีชีวิตได้โดยไม่ต้องทำซ้ำตรรกะด้วยตนเอง
การแบ่งส่วนเสมือนของคลาสบางส่วน
บางสิ่งที่ฉันทำเมื่อหลายปีก่อนซึ่งแย่จริงๆและฉันก็ไม่แน่ใจด้วยซ้ำว่ามันจะมีประโยชน์อีกต่อไป ในกรณีนี้เราได้เก็บ ID คลาสไว้กับแต่ละอินสแตนซ์เพื่อวัตถุประสงค์อื่นแล้ว (เข้าถึงผ่าน accessor ในคลาสพื้นฐานซึ่งไม่ใช่แบบเสมือน) ที่นั่นเราทำบางสิ่งที่คล้ายคลึงกับสิ่งนี้ (ความทรงจำของฉันมืดครึ้มเล็กน้อย):
switch (obj->type())
{
case id_common_type:
static_cast<CommonType*>(obj)->non_virtual_do_something();
break;
...
default:
obj->virtual_do_something();
break;
}
... ซึ่งvirtual_do_something
ถูกนำไปใช้เพื่อเรียกใช้เวอร์ชันที่ไม่ใช่เสมือนในคลาสย่อย ฉันรู้ว่ามันแย่มากการทำ downcast แบบชัดแจ้งเพื่อพัฒนาการเรียกใช้ฟังก์ชัน ฉันไม่รู้ว่ามันมีประโยชน์แค่ไหนในตอนนี้เพราะฉันไม่ได้ลองสิ่งนี้มาหลายปีแล้ว ด้วยการเปิดรับการออกแบบที่เน้นข้อมูลฉันพบกลยุทธ์ข้างต้นในการแยกโครงสร้างข้อมูลและวนรอบในแบบร้อน / เย็นเพื่อให้มีประโยชน์มากกว่าเปิดประตูสำหรับกลยุทธ์การเพิ่มประสิทธิภาพ (และน่าเกลียดน้อยกว่า)
การขายส่งเสมือนจริง
ฉันต้องยอมรับว่าฉันไม่เคยใช้ความคิดการปรับให้เหมาะสมที่สุดมาก่อนดังนั้นฉันจึงไม่ทราบถึงประโยชน์ที่ได้รับ ฉันได้หลีกเลี่ยงฟังก์ชั่นทางอ้อมในการมองการณ์ไกลในกรณีที่ฉันรู้ว่าจะมีเพียงหนึ่งชุดของเงื่อนไข (เช่นการประมวลผลเหตุการณ์ที่มีการประมวลผลกลางเดียวเท่านั้น) แต่ไม่เคยเริ่มด้วยความคิดแบบ polymorphic และปรับให้เหมาะสมที่สุด ถึงที่นี่
ในทางทฤษฎีประโยชน์ทันทีที่นี่อาจเป็นวิธีที่มีขนาดเล็กลงในการระบุชนิดของตัวชี้เสมือน (เช่น: ไบต์เดียวถ้าคุณสามารถยอมรับความคิดที่ว่ามี 256 ชนิดที่ไม่ซ้ำกันหรือน้อยกว่า) เพื่อกำจัดอุปสรรคการเพิ่มประสิทธิภาพเหล่านี้อย่างสมบูรณ์ .
นอกจากนี้ยังอาจช่วยในการเขียนรหัสที่ง่ายต่อการบำรุงรักษา (เทียบกับตัวอย่างการปรับแก้ด้วยตนเองด้วยตนเองด้านบน) หากคุณใช้switch
คำสั่งกลางเดียวโดยไม่ต้องแยกโครงสร้างข้อมูลและลูปตามประเภทย่อยหรือหากมีคำสั่ง - การพึ่งพาในกรณีเหล่านี้ซึ่งสิ่งต่าง ๆ จะต้องถูกประมวลผลในลำดับที่ถูกต้อง (แม้ว่าสิ่งนั้นจะทำให้เราแตกสาขาทั่วสถานที่) switch
นี้จะเป็นกรณีที่คุณไม่ได้มีมากเกินไปสถานที่ที่ต้องทำ
โดยทั่วไปฉันจะไม่แนะนำสิ่งนี้แม้ว่าจะมีความคิดที่มีประสิทธิภาพมาก ๆ เว้นแต่ว่าจะง่ายต่อการบำรุงรักษา "ง่ายต่อการบำรุงรักษา" มีแนวโน้มที่จะขึ้นอยู่กับปัจจัยสำคัญสองประการ:
- ไม่มีความต้องการความสามารถในการขยายที่แท้จริง (เช่นรู้ว่าคุณมีสิ่งที่ต้องดำเนินการทั้งหมด 8 ประเภทและไม่ต้องทำอะไรอีกต่อไป)
- ไม่มีสถานที่มากมายในรหัสของคุณที่ต้องตรวจสอบประเภทเหล่านี้ (เช่นสถานที่ส่วนกลางหนึ่งแห่ง)
... แต่ฉันขอแนะนำสถานการณ์ด้านบนในกรณีส่วนใหญ่และทำซ้ำเพื่อหาวิธีแก้ปัญหาที่มีประสิทธิภาพมากขึ้นโดยการพัฒนาส่วนเสมือนตามต้องการ มันช่วยให้คุณมีห้องหายใจมากขึ้นเพื่อสร้างสมดุลระหว่างความต้องการการบำรุงรักษาและความสามารถในการบำรุงรักษากับประสิทธิภาพ
ฟังก์ชั่นเสมือนกับตัวชี้ฟังก์ชั่น
เพื่อปิดด้านบนนี้ฉันสังเกตเห็นที่นี่ว่ามีการสนทนาบางอย่างเกี่ยวกับฟังก์ชั่นเสมือนกับตัวชี้ฟังก์ชั่น เป็นความจริงที่ว่าฟังก์ชั่นเสมือนจริงต้องใช้งานเพิ่มเติมเล็กน้อยในการโทร แต่นั่นไม่ได้หมายความว่าจะช้าลง มันอาจทำให้พวกมันเร็วขึ้น
มันตอบโต้ได้ง่ายที่นี่เพราะเราเคยชินกับการวัดค่าใช้จ่ายในแง่ของคำแนะนำโดยไม่ให้ความสนใจกับพลวัตของลำดับชั้นความจำซึ่งมักจะมีผลกระทบที่สำคัญกว่า
หากเราเปรียบเทียบ a class
กับ 20 ฟังก์ชั่นเสมือนกับstruct
ที่เก็บ 20 ตัวชี้ฟังก์ชั่นและทั้งสองอินสแตนซ์หลายครั้งหน่วยความจำค่าใช้จ่ายของแต่ละclass
อินสแตนซ์ในกรณีนี้ 8 ไบต์สำหรับตัวชี้เสมือนบนเครื่อง 64 บิตในขณะที่หน่วยความจำ ค่าใช้จ่ายของstruct
160 ไบต์
ค่าใช้จ่ายในทางปฏิบัติอาจมีแคชที่บังคับและไม่บังคับมากกว่าหายไปกับตารางของตัวชี้ฟังก์ชันกับคลาสที่ใช้ฟังก์ชันเสมือน (และอาจมีข้อบกพร่องของหน้ากระดาษที่ขนาดอินพุตที่ใหญ่พอ) ค่าใช้จ่ายมีแนวโน้มที่จะแคระงานพิเศษเล็กน้อยในการจัดทำดัชนีตารางเสมือน
ฉันยังจัดการกับโค้ด C ดั้งเดิม (เก่ากว่าฉัน) ซึ่งการเปลี่ยนที่structs
เต็มไปด้วยพอยน์เตอร์ฟังก์ชั่นและอินสแตนซ์หลายครั้งจริง ๆ แล้วให้ผลการปฏิบัติงานที่สำคัญ (มากกว่า 100% ปรับปรุง) โดยเปลี่ยนพวกมันให้เป็นคลาสด้วยฟังก์ชันเสมือน เนื่องจากการลดการใช้หน่วยความจำขนาดใหญ่, เพิ่มความเป็นมิตรกับแคช ฯลฯ
ในทางกลับกันเมื่อการเปรียบเทียบมากขึ้นเกี่ยวกับแอปเปิ้ลกับแอปเปิ้ลฉันก็พบว่าความคิดตรงกันข้ามของการแปลจากชุดฟังก์ชั่นเสมือนจริงของ C ++ ไปเป็นแนวคิดตัวชี้ฟังก์ชั่นรูปแบบ C
class Functionoid
{
public:
virtual ~Functionoid() {}
virtual void operator()() = 0;
};
... ซึ่งคลาสนั้นถูกเก็บฟังก์ชัน overridable ที่เลวทรามต่ำช้า (หรือสองถ้าเรานับ destructor เสมือน) ในกรณีเหล่านั้นสามารถช่วยในเส้นทางที่สำคัญในการเปลี่ยนสิ่งนี้เป็น:
void (*func_ptr)(void* instance_data);
... void*
ไปความนึกคิดที่อยู่เบื้องหลังอินเตอร์เฟซชนิดปลอดภัยเพื่อซ่อนปลดเปลื้องอันตรายจาก
ในกรณีที่เราถูกล่อลวงให้ใช้คลาสที่มีฟังก์ชั่นเสมือนเดียวมันสามารถช่วยให้ใช้ตัวชี้ฟังก์ชันแทนได้อย่างรวดเร็ว เหตุผลใหญ่ไม่จำเป็นต้องมีค่าใช้จ่ายลดลงแม้แต่น้อยในการโทรหาตัวชี้ฟังก์ชัน เป็นเพราะเราไม่ต้องเผชิญกับสิ่งล่อใจที่จะจัดสรรแต่ละ functionoid แยกต่างหากในพื้นที่กระจัดกระจายของกองถ้าเรารวมพวกเขาเข้าไปในโครงสร้างถาวร วิธีการแบบนี้สามารถทำให้ง่ายขึ้นในการหลีกเลี่ยงการเชื่อมโยงกับฮีปและโอเวอร์เฮดการกระจายตัวของหน่วยความจำหากข้อมูลอินสแตนซ์เป็นเนื้อเดียวกันเช่นและพฤติกรรมที่แตกต่างกันเท่านั้น
มีบางกรณีที่การใช้งานพอยน์เตอร์พอยน์เตอร์สามารถช่วยได้ แต่บ่อยครั้งที่ฉันพบว่ามันเป็นวิธีอื่นถ้าเราเปรียบเทียบพอยน์เตอร์ของฟังก์ชั่นหลาย ๆ อันกับ vtable เดี่ยวซึ่งต้องการเพียงหนึ่งพอยน์เตอร์เท่านั้น . vtable นั้นมักจะนั่งในหนึ่งหรือมากกว่าหนึ่งแคช L1 line และในลูปที่แน่น
ข้อสรุป
ดังนั้นนั่นคือการหมุนเล็กน้อยของฉันในหัวข้อนี้ ฉันขอแนะนำให้ระบายในพื้นที่เหล่านี้ด้วยความระมัดระวัง การวัดความน่าเชื่อถือไม่ใช่สัญชาตญาณและวิธีที่การเพิ่มประสิทธิภาพเหล่านี้มักจะลดความสามารถในการบำรุงรักษาเพียงเท่าที่คุณสามารถจ่ายได้