โดยทั่วไปแล้วควรใช้ฟังก์ชั่นเสมือนเพื่อหลีกเลี่ยงการแตกแขนงหรือไม่?


21

ดูเหมือนว่าจะมีคำสั่งที่เทียบเท่ากันอย่างคร่าว ๆ เพื่อแบ่งให้เท่ากันกับค่าใช้จ่ายของฟังก์ชั่นเสมือนสาขาพลาดมีการแลกเปลี่ยนที่คล้ายกัน:

  • การเรียนการสอนเทียบกับแคชข้อมูลพลาด
  • อุปสรรคการเพิ่มประสิทธิภาพ

ถ้าคุณมองสิ่งที่ชอบ:

if (x==1) {
   p->do1();
}
else if (x==2) {
   p->do2();
}
else if (x==3) {
   p->do3();
}
...

คุณอาจมีฟังก์ชั่นสมาชิกอาร์เรย์หรือถ้ามีหลายฟังก์ชั่นขึ้นอยู่กับการจัดหมวดหมู่เดียวกันหรือมีการจัดหมวดหมู่ที่ซับซ้อนมากขึ้นให้ใช้ฟังก์ชั่นเสมือน:

p->do()

แต่โดยทั่วไปว่าราคาแพงมีหน้าที่เสมือน VS แตกแขนงมันเป็นเรื่องยากที่จะทดสอบบนแพลตฟอร์มพอที่จะพูดคุยเพื่อให้ผมสงสัยว่าถ้าคนใดคนหนึ่งมีกฎหยาบของนิ้วหัวแม่มือ (น่ารักถ้ามันเป็นง่ายๆเป็น 4 ifคือเบรกพอยต์)

โดยทั่วไปแล้วฟังก์ชั่นเสมือนจะชัดเจนกว่าและฉันก็จะเอนไปทางพวกเขา แต่ฉันมีหลายส่วนที่สำคัญมากซึ่งฉันสามารถเปลี่ยนรหัสจากฟังก์ชั่นเสมือนเป็นสาขาได้ ฉันต้องการที่จะมีความคิดเกี่ยวกับเรื่องนี้ก่อนที่จะดำเนินการนี้ (ไม่ใช่การเปลี่ยนแปลงเล็กน้อยหรือง่ายต่อการทดสอบในหลาย ๆ แพลตฟอร์ม)


12
ความต้องการด้านประสิทธิภาพของคุณคืออะไร? คุณมีตัวเลขจำนวนมากที่คุณต้องตีหรือคุณมีส่วนร่วมในการเพิ่มประสิทธิภาพก่อนวัยอันควร? ทั้งวิธีการแยกและวิธีเสมือนนั้นมีราคาถูกมากในรูปแบบที่ยิ่งใหญ่ของสิ่งต่าง ๆ (เช่นเมื่อเปรียบเทียบกับอัลกอริธึมที่ไม่ดี, I / O หรือการจัดสรรฮีป)
amon

4
ทำอะไรก็ได้ที่อ่านได้ / ยืดหยุ่น / ไม่น่าจะเป็นไปเพื่อการเปลี่ยนแปลงในอนาคตมากขึ้นและเมื่อคุณได้ทำงานแล้วให้ทำโปรไฟล์และดูว่าเรื่องนี้สำคัญจริง ๆ หรือไม่ มักจะไม่
Ixrec

1
คำถาม: "แต่โดยทั่วไปแล้วฟังก์ชั่นเสมือนแพงแค่ไหน ... "คำตอบ: สาขาทางอ้อม (วิกิพีเดีย)
rwong

1
โปรดจำไว้ว่าคำตอบส่วนใหญ่ขึ้นอยู่กับการนับจำนวนคำสั่ง ในฐานะที่เป็นเครื่องมือเพิ่มประสิทธิภาพระดับต่ำฉันไม่เชื่อถือจำนวนคำสั่ง คุณต้องพิสูจน์พวกเขาในสถาปัตยกรรม CPU เฉพาะ - ทางร่างกาย - ภายใต้เงื่อนไขการทดลอง คำตอบที่ถูกต้องสำหรับคำถามนี้ต้องเป็นเชิงประจักษ์และเชิงทดลองไม่ใช่เชิงทฤษฎี

3
ปัญหาของคำถามนี้คือมันเป็นข้อสันนิษฐานว่ามันใหญ่พอที่จะกังวล ในซอฟต์แวร์จริงปัญหาด้านประสิทธิภาพมาเป็นชิ้นใหญ่เช่นชิ้นพิซซ่าหลายขนาด ยกตัวอย่างเช่นดูที่นี่ อย่าคิดว่าคุณรู้ว่าปัญหาที่ใหญ่ที่สุดคืออะไร - ให้โปรแกรมบอกคุณ แก้ไขจากนั้นให้มันบอกคุณว่าอันถัดไปคืออะไร ทำครึ่งโหลครั้งนี้และคุณอาจลงไปที่การเรียกฟังก์ชั่นเสมือนมีค่าน่าเป็นห่วง ในประสบการณ์ของฉันพวกเขาไม่เคยมี
Mike Dunlavey

คำตอบ:


21

ฉันต้องการที่จะกระโดดเข้าไปที่นี่ท่ามกลางคำตอบที่ยอดเยี่ยมแล้วและยอมรับว่าฉันได้ใช้วิธีที่น่าเกลียดของการทำงานย้อนกลับไปยังรูปแบบการต่อต้านการเปลี่ยนรหัส 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 บิตในขณะที่หน่วยความจำ ค่าใช้จ่ายของstruct160 ไบต์

ค่าใช้จ่ายในทางปฏิบัติอาจมีแคชที่บังคับและไม่บังคับมากกว่าหายไปกับตารางของตัวชี้ฟังก์ชันกับคลาสที่ใช้ฟังก์ชันเสมือน (และอาจมีข้อบกพร่องของหน้ากระดาษที่ขนาดอินพุตที่ใหญ่พอ) ค่าใช้จ่ายมีแนวโน้มที่จะแคระงานพิเศษเล็กน้อยในการจัดทำดัชนีตารางเสมือน

ฉันยังจัดการกับโค้ด 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 และในลูปที่แน่น

ข้อสรุป

ดังนั้นนั่นคือการหมุนเล็กน้อยของฉันในหัวข้อนี้ ฉันขอแนะนำให้ระบายในพื้นที่เหล่านี้ด้วยความระมัดระวัง การวัดความน่าเชื่อถือไม่ใช่สัญชาตญาณและวิธีที่การเพิ่มประสิทธิภาพเหล่านี้มักจะลดความสามารถในการบำรุงรักษาเพียงเท่าที่คุณสามารถจ่ายได้


ฟังก์ชั่นเสมือนจริงเป็นตัวชี้ฟังก์ชั่นเพียงนำไปใช้ในชั้นเรียนที่ปฏิบัติได้ เมื่อมีการเรียกใช้ฟังก์ชันเสมือนจะมีการค้นหาในเด็กและห่วงโซ่การสืบทอดก่อน นี่คือเหตุผลที่มรดกล้ำลึกมีราคาแพงมากและมักจะหลีกเลี่ยงใน c ++
Robert Baron

@RobertBaron: ฉันไม่เคยเห็นการใช้งานฟังก์ชั่นเสมือนจริงอย่างที่คุณพูด (= ด้วยการค้นหาเชนผ่านลำดับชั้นของคลาส) โดยทั่วไปแล้วคอมไพเลอร์เพียงสร้าง vatt "flattened" สำหรับคอนกรีตแต่ละประเภทที่มีพอยน์เตอร์ฟังก์ชั่นที่ถูกต้องทั้งหมดและในการใช้งานจริงการโทรจะได้รับการแก้ไขด้วยการค้นหาแบบตารางเดี่ยว ไม่มีการจ่ายค่าปรับสำหรับลำดับชั้นมรดกที่ลึก
Matteo Italia

มัตเตโอนี่เป็นคำอธิบายที่นักเทคนิคนำให้เมื่อหลายปีก่อน จริงอยู่มันเป็น c ++ ดังนั้นเขาอาจพิจารณาถึงความหมายของมรดกหลาย ๆ อย่าง ขอบคุณสำหรับการชี้แจงความเข้าใจของฉันเกี่ยวกับวิธีการปรับ vtables
Robert Baron

ขอบคุณสำหรับคำตอบที่ดี (+1) ฉันสงสัยว่าสิ่งนี้มีประโยชน์กับ std :: Visit มากกว่าหน้าที่เสมือนจริงเพียงใด
DaveFar

13

ข้อสังเกต:

  • ในหลาย ๆ กรณีฟังก์ชั่นเสมือนนั้นเร็วกว่าเพราะการค้นหา vtable เป็นการO(1)ดำเนินการในขณะที่else if()บันไดเป็นการO(n)ดำเนินการ อย่างไรก็ตามนี่เป็นความจริงเฉพาะในกรณีที่การกระจายของคดีแบน

  • สำหรับif() ... elseเงื่อนไขเงื่อนไขนั้นเร็วกว่าเพราะคุณบันทึกการเรียกใช้ฟังก์ชัน

  • ดังนั้นเมื่อคุณมีการแจกแจงแบบแบน ๆ ของเคสจุดแตกหักจะต้องมีอยู่ คำถามเดียวคือที่ตั้งของมัน

  • ถ้าคุณใช้switch()แทนelse if()บันไดหรือฟังก์ชั่นเสมือนสายคอมไพเลอร์ของคุณอาจผลิตรหัสที่ดียิ่งขึ้น: มันสามารถทำสาขาไปยังสถานที่ซึ่งจะเงยหน้าขึ้นจากตาราง แต่ที่ไม่ได้เป็นฟังก์ชั่นการโทร นั่นคือคุณมีคุณสมบัติทั้งหมดของการเรียกใช้ฟังก์ชันเสมือนโดยไม่มีการเรียกใช้ฟังก์ชันทั้งหมด

  • ถ้าหากพบบ่อยกว่าส่วนที่เหลือการเริ่มต้นif() ... elseด้วยกรณีนั้นจะให้ประสิทธิภาพที่ดีที่สุดแก่คุณ: คุณจะดำเนินการสาขาที่มีเงื่อนไขเดียวซึ่งคาดการณ์ได้อย่างถูกต้องในกรณีส่วนใหญ่

  • คอมไพเลอร์ของคุณไม่มีความรู้เกี่ยวกับการแจกแจงที่คาดหวังของกรณีและจะถือว่าการกระจายแบน

เนื่องจากคอมไพเลอร์ของคุณน่าจะมีฮิวริสติกที่ดีอยู่บ้างเมื่อใช้โค้ดswitch()เป็นelse if()บันไดหรือเป็นตารางการค้นหา ฉันมักจะเชื่อใจการตัดสินใจของมันจนกว่าคุณจะรู้ว่าการแจกแจงคดีมีความลำเอียง

ดังนั้นคำแนะนำของฉันคือ:

  • หากกรณีใดกรณีหนึ่งแคระส่วนที่เหลือในแง่ของความถี่ใช้else if()บันไดเรียง

  • มิฉะนั้นให้ใช้switch()คำสั่งเว้นแต่วิธีใดวิธีหนึ่งทำให้รหัสของคุณอ่านง่ายขึ้น ตรวจสอบให้แน่ใจว่าคุณไม่ได้ซื้อประสิทธิภาพที่ไม่อาจปฏิเสธได้ด้วยการอ่านที่ลดลงอย่างมาก

  • หากคุณใช้ a switch()และยังไม่พอใจกับประสิทธิภาพให้ทำการเปรียบเทียบ แต่ต้องเตรียมพร้อมที่จะค้นหาว่าสิ่งนั้นswitch()เป็นไปได้ที่เร็วที่สุดแล้ว


2
คอมไพเลอร์บางตัวอนุญาตให้ใช้คำอธิบายประกอบเพื่อบอกคอมไพเลอร์ว่ากรณีใดมีแนวโน้มที่จะเป็นจริงและคอมไพเลอร์เหล่านั้นสามารถสร้างโค้ดได้เร็วขึ้นตราบใดที่คำอธิบายประกอบนั้นถูกต้อง
gnasher729

5
การดำเนินการ O (1) ไม่จำเป็นต้องเร็วขึ้นในเวลาดำเนินการในโลกแห่งความเป็นจริงกว่า O (n) หรือแม้กระทั่ง O (n ^ 20)
whatsisname

2
@whatsisname นั่นเป็นเหตุผลที่ฉันพูดว่า "สำหรับหลาย ๆ กรณี" ตามคำนิยามของO(1)และO(n)มีอยู่kเพื่อให้O(n)การทำงานมากกว่าฟังก์ชั่นสำหรับทุกO(1) n >= kคำถามเดียวก็คือคุณมีแนวโน้มที่จะมีหลายกรณีหรือไม่ และใช่ฉันเคยเห็นswitch()ข้อความที่มีหลายกรณีว่าelse if()บันไดนั้นช้ากว่าการเรียกฟังก์ชันเสมือนหรือการแจกจ่ายที่โหลด
cmaster - คืนสถานะโมนิก้า

ปัญหาที่ฉันมีกับคำตอบนี้เป็นคำเตือนเพียงอย่างเดียวในการตัดสินใจโดยยึดตามผลการดำเนินงานที่ไม่เกี่ยวข้องอย่างสมบูรณ์ถูกซ่อนอยู่ที่ไหนสักแห่งในย่อหน้าถัดไป ทุกอย่างอื่นที่นี่ทำท่าว่ามันเป็นความคิดที่ดีที่จะตัดสินใจเกี่ยวifกับการswitchเปรียบเทียบกับฟังก์ชั่นเสมือนจริงขึ้นอยู่กับประสิทธิภาพ ในกรณีที่หายากมากอาจเป็นได้ แต่ในกรณีส่วนใหญ่จะไม่เป็นเช่นนั้น
Doc Brown

7

โดยทั่วไปแล้วควรใช้ฟังก์ชั่นเสมือนเพื่อหลีกเลี่ยงการแตกแขนงหรือไม่?

โดยทั่วไปแล้วใช่ ประโยชน์สำหรับการบำรุงรักษามีความสำคัญ (การทดสอบในการแยกการแยกความกังวลการแยกส่วนแบบแยกส่วนที่ปรับปรุงแล้วและความสามารถในการขยาย)

แต่โดยทั่วไปแล้วฟังก์ชั่นเวอร์ชวลกับแบรนดิ้งนั้นมีราคาแพงแค่ไหนมันเป็นการยากที่จะทดสอบบนแพลตฟอร์มที่เพียงพอที่จะพูดคุยกันทั่วไปดังนั้นฉันจึงสงสัยว่าใครมีกฎง่ายๆของนิ้วหัวแม่มือ (น่ารัก

ถ้าคุณไม่ทำโปรไฟล์ของคุณและรู้ว่าการจัดส่งระหว่างสาขา ( การประเมินเงื่อนไข ) ใช้เวลามากกว่าการคำนวณที่ทำ ( รหัสในสาขา ) ให้ปรับการคำนวณให้เหมาะสม

นั่นคือคำตอบที่ถูกต้องในการ "ฟังก์ชั่นเสมือนกับการเปรียบเทียบกับการแตกแขนง" ที่ถูกต้องคือการวัดและค้นหา

Rule of thumb : ยกเว้นกรณีที่มีสถานการณ์ข้างต้น (การเลือกปฏิบัติสาขามีราคาแพงกว่าการคำนวณสาขา) เพิ่มประสิทธิภาพส่วนของรหัสนี้สำหรับความพยายามในการบำรุงรักษา (ใช้ฟังก์ชั่นเสมือน)

คุณบอกว่าคุณต้องการให้ส่วนนี้ทำงานเร็วที่สุดเท่าที่จะเป็นไปได้ เร็วแค่ไหน? ความต้องการที่เป็นรูปธรรมของคุณคืออะไร

โดยทั่วไปแล้วฟังก์ชั่นเสมือนจะชัดเจนกว่าและฉันก็จะเอนไปทางพวกเขา แต่ฉันมีหลายส่วนที่สำคัญมากซึ่งฉันสามารถเปลี่ยนรหัสจากฟังก์ชั่นเสมือนเป็นสาขาได้ ฉันต้องการที่จะมีความคิดเกี่ยวกับเรื่องนี้ก่อนที่จะดำเนินการนี้ (ไม่ใช่การเปลี่ยนแปลงเล็กน้อยหรือง่ายต่อการทดสอบในหลาย ๆ แพลตฟอร์ม)

ใช้ฟังก์ชั่นเสมือนจริง สิ่งนี้จะช่วยให้คุณสามารถปรับให้เหมาะสมสำหรับแต่ละแพลตฟอร์มหากจำเป็นและยังคงรักษารหัสลูกค้าให้สะอาดอยู่เสมอ


หลังจากเขียนโปรแกรมการบำรุงรักษาจำนวนมากแล้วฉันจะพูดด้วยความระมัดระวังเล็กน้อย: ฟังก์ชั่นเสมือน IMNSHO นั้นค่อนข้างแย่สำหรับการบำรุงรักษาเนื่องจากข้อได้เปรียบของคุณ ปัญหาหลักคือความยืดหยุ่น คุณสามารถติดอะไรในนั้น ... และผู้คนทำ มันยากมากที่จะมีเหตุผลเกี่ยวกับการจัดส่งแบบไดนามิก แต่ในกรณีส่วนใหญ่โค้ดไม่ต้องการความยืดหยุ่นทั้งหมดและการลบความยืดหยุ่นรันไทม์สามารถทำให้เหตุผลเกี่ยวกับโค้ดง่ายขึ้น แต่ฉันไม่ต้องการไปไกลเท่าที่จะบอกว่าคุณไม่ควรใช้การจัดส่งแบบไดนามิก; นั่นไร้สาระ
Eamon Nerbonne

abstractions ที่ดีที่สุดในการทำงานกับเป็นที่หายาก (เช่น codebase มี abstractions ทึบแสงเพียงไม่กี่) แต่ super-duper แข็งแกร่ง โดยทั่วไป: อย่าติดอะไรอยู่ข้างหลังนามธรรมการจัดส่งแบบไดนามิกเพียงเพราะมันมีรูปร่างคล้ายกันสำหรับกรณีใดกรณีหนึ่งโดยเฉพาะ เพียง แต่ทำดังนั้นหากคุณไม่สามารถตั้งครรภ์มีเหตุผลใดเหตุผลที่จะเคยดูแลเกี่ยวกับความแตกต่างระหว่างวัตถุที่ใช้งานร่วมกันอินเตอร์เฟซที่ใด ๆ หากคุณไม่สามารถ: ดีกว่าที่จะมีผู้ช่วยที่ไม่ห่อหุ้มสิ่งที่ดีกว่าสิ่งที่เป็นนามธรรม และจากนั้น; มีความแตกต่างระหว่างความยืดหยุ่นของรันไทม์และความยืดหยุ่นของโค้ดเบส
Eamon Nerbonne

5

คำตอบอื่น ๆ ให้ข้อโต้แย้งทางทฤษฎีที่ดีอยู่แล้ว ฉันต้องการเพิ่มผลลัพธ์ของการทดสอบที่ฉันได้ดำเนินการเมื่อเร็ว ๆ นี้เพื่อประเมินว่าจะเป็นการดีหรือไม่ที่จะใช้ virtual machine (VM) โดยใช้switchop-code ขนาดใหญ่หรือตีความ op-code เป็นดัชนีแทน เป็นอาร์เรย์ของพอยน์เตอร์ของฟังก์ชัน แม้ว่านี่จะไม่เหมือนกับการvirtualเรียกใช้ฟังก์ชัน แต่ฉันคิดว่ามันใกล้พอสมควร

ฉันได้เขียนสคริปต์ Python เพื่อสร้างรหัส C ++ 14 แบบสุ่มสำหรับ VM ด้วยขนาดชุดคำสั่งที่เลือกแบบสุ่ม (แม้ว่าจะไม่เหมือนกัน แต่สุ่มช่วงที่มีความหนาแน่นน้อยกว่า) ระหว่าง 1 ถึง 10,000 VM ที่สร้างขึ้นมักมี 128 รีจิสเตอร์และไม่ แกะ. คำแนะนำนั้นไม่มีความหมายและทุกคนมีแบบฟอร์มต่อไปนี้

inline void
op0004(machine_state& state) noexcept
{
  const auto c = word_t {0xcf2802e8d0baca1dUL};
  const auto r1 = state.registers[58];
  const auto r2 = state.registers[69];
  const auto r3 = ((r1 + c) | r2);
  state.registers[6] = r3;
}

สคริปต์ยังสร้างรูทีนการจัดส่งโดยใช้switchคำสั่ง ...

inline int
dispatch(machine_state& state, const opcode_t opcode) noexcept
{
  switch (opcode)
  {
  case 0x0000: op0000(state); return 0;
  case 0x0001: op0001(state); return 0;
  // ...
  case 0x247a: op247a(state); return 0;
  case 0x247b: op247b(state); return 0;
  default:
    return -1;  // invalid opcode
  }
}

... และพอยน์เตอร์ของฟังก์ชันอาร์เรย์

inline int
dispatch(machine_state& state, const opcode_t opcode) noexcept
{
  typedef void (* func_type)(machine_state&);
  static const func_type table[VM_NUM_INSTRUCTIONS] = {
    op0000,
    op0001,
    // ...
    op247a,
    op247b,
  };
  if (opcode >= VM_NUM_INSTRUCTIONS)
    return -1;  // invalid opcode
  table[opcode](state);
  return 0;
}

รูทีนการจัดส่งใดที่ถูกสร้างขึ้นถูกเลือกแบบสุ่มสำหรับแต่ละ VM ที่สร้างขึ้น

สำหรับการเปรียบเทียบกระแสข้อมูลของรหัสถูกสร้างขึ้นโดยการสุ่มเมล็ด ( std::random_device) Mersenne twister random engine ( std::mt19937_64)

สำหรับ VM แต่ละที่ได้รับการรวบรวมกับ GCC 5.2.0 ใช้-DNDEBUG, -O3และ-std=c++14สวิทช์ ขั้นแรกรวบรวมข้อมูลโดยใช้-fprofile-generateตัวเลือกและข้อมูลโปรไฟล์ที่รวบรวมเพื่อจำลองคำแนะนำแบบสุ่ม 1,000 คำ จากนั้นโค้ดจะถูกคอมไพล์ใหม่ด้วย-fprofile-useตัวเลือกที่อนุญาตการปรับให้เหมาะสมตามข้อมูลโปรไฟล์ที่รวบรวม

จากนั้น VM ก็ทำการออกกำลังกาย (ในกระบวนการเดียวกัน) สี่ครั้งสำหรับ 50,000 000 รอบและเวลาสำหรับการทดสอบแต่ละครั้ง การดำเนินการครั้งแรกถูกยกเลิกเพื่อกำจัดเอฟเฟกต์ Cold-cache PRNG ไม่ได้ถูก seed อีกครั้งระหว่างการรันเพื่อให้พวกเขาไม่ได้ดำเนินการตามคำสั่งในลำดับเดียวกัน

เมื่อใช้การตั้งค่านี้จะมีการรวบรวม 1,000 จุดข้อมูลสำหรับแต่ละรูทีนการส่ง ข้อมูลถูกรวบรวมบน Quad Core AMD A8-6600K APU พร้อมแคช 2048 KiB ที่รัน GNU / Linux 64 บิตโดยไม่มีเดสก์ท็อปกราฟิกหรือโปรแกรมอื่น ๆ ที่ทำงานอยู่ แสดงด้านล่างคือพล็อตของเวลา CPU เฉลี่ย (พร้อมส่วนเบี่ยงเบนมาตรฐาน) ต่อคำสั่งสำหรับ VM แต่ละเครื่อง

ป้อนคำอธิบายรูปภาพที่นี่

จากข้อมูลนี้ฉันสามารถมั่นใจได้ว่าการใช้ตารางฟังก์ชั่นเป็นความคิดที่ดียกเว้นอาจจะมีรหัส op จำนวนน้อยมาก ฉันไม่มีคำอธิบายสำหรับค่าผิดปกติของswitchเวอร์ชันระหว่าง 500 ถึง 1,000 คำแนะนำ

รหัสทั้งหมดแหล่งสำหรับมาตรฐานเช่นเดียวกับข้อมูลการทดลองที่เต็มรูปแบบและพล็อตความละเอียดสูงที่สามารถพบได้บนเว็บไซต์ของฉัน


3

นอกเหนือจากคำตอบที่ดีของ cmaster ที่ฉัน upvoted พึงระลึกไว้เสมอว่าตัวชี้ฟังก์ชั่นนั้นจะเร็วกว่าฟังก์ชั่นเสมือนอย่างเคร่งครัด การจัดส่งฟังก์ชันเสมือนโดยทั่วไปเกี่ยวข้องกับการติดตามตัวชี้จากวัตถุไปยัง vtable ก่อนการจัดทำดัชนีอย่างเหมาะสมจากนั้นยกเลิกการเรียกใช้ตัวชี้ฟังก์ชัน ดังนั้นขั้นตอนสุดท้ายจะเหมือนกัน แต่มีขั้นตอนเพิ่มเติมในขั้นต้น นอกจากนี้ฟังก์ชั่นเสมือนใช้ "นี้" เสมอเป็นอาร์กิวเมนต์ตัวชี้ฟังก์ชั่นมีความยืดหยุ่นมากขึ้น

สิ่งอื่นที่ควรทราบ: หากเส้นทางที่สำคัญของคุณเกี่ยวข้องกับการวนรอบมันจะมีประโยชน์ในการเรียงลำดับการวนรอบโดยการส่งปลายทาง เห็นได้ชัดว่านี่คือ nlogn ในขณะที่การวนลูปเป็นเพียง n แต่ถ้าคุณกำลังสำรวจหลาย ๆ ครั้งมันจะคุ้มค่า โดยการเรียงลำดับตามการจัดส่งปลายทางคุณมั่นใจได้ว่ามีการเรียกใช้รหัสเดียวกันซ้ำ ๆ กันทำให้ร้อนใน icache ลดการพลาดแคชให้น้อยที่สุด

กลยุทธ์ที่สามที่ต้องคำนึงถึง: หากคุณตัดสินใจที่จะย้ายออกจากฟังก์ชันเสมือน / ตัวชี้ฟังก์ชันไปสู่กลยุทธ์ / สวิตช์คุณอาจได้รับการบริการที่ดีโดยการเปลี่ยนจากวัตถุ polymorphic ไปเป็นสิ่งที่ชอบ boost :: variant (ซึ่งให้สวิตช์ กรณีในรูปแบบของนามธรรมผู้เข้าชม) วัตถุ Polymorphic จะต้องจัดเก็บโดยตัวชี้ฐานดังนั้นข้อมูลของคุณจึงอยู่ในแคช สิ่งนี้อาจมีอิทธิพลอย่างมากต่อเส้นทางสำคัญของคุณมากกว่าค่าใช้จ่ายของการค้นหาเสมือน ในขณะที่ตัวแปรถูกเก็บแบบอินไลน์เป็นสหภาพที่แบ่งแยก มันมีขนาดเท่ากับประเภทข้อมูลที่ใหญ่ที่สุด (บวกค่าคงที่ขนาดเล็ก) หากวัตถุของคุณมีขนาดไม่ใหญ่เกินไปนี่เป็นวิธีที่ยอดเยี่ยมในการจัดการกับวัตถุเหล่านั้น

ที่จริงแล้วฉันไม่แปลกใจเลยถ้าการปรับปรุงความสอดคล้องกันของข้อมูลของคุณจะมีผลกระทบที่ยิ่งใหญ่กว่าคำถามเดิมของคุณ


ฉันไม่ทราบว่าฟังก์ชั่นเสมือนเกี่ยวข้องกับ "ขั้นตอนพิเศษ" แม้ว่า ระบุว่ารูปแบบของชั้นเรียนเป็นที่รู้จักกันในเวลารวบรวมมันเป็นหลักเช่นเดียวกับการเข้าถึงอาร์เรย์ คือมีตัวชี้ไปยังด้านบนของคลาสและรู้จักออฟเซ็ตของฟังก์ชันดังนั้นเพียงแค่เพิ่มเข้าไปอ่านผลลัพธ์และนั่นคือที่อยู่ ค่าใช้จ่ายไม่มาก

1
มันเกี่ยวข้องกับขั้นตอนพิเศษ ตัว vtable นั้นมีพอยน์เตอร์ของฟังก์ชั่นดังนั้นเมื่อคุณไปที่ vtable คุณได้เข้าสู่สถานะเดียวกับที่คุณเริ่มด้วยตัวชี้ฟังก์ชั่น ทุกอย่างก่อนที่คุณจะไปที่ vtable นั้นเป็นงานพิเศษ คลาสไม่ได้มี vtables พวกมันมีพอยน์เตอร์ไปยัง vtables และการติดตามตัวชี้นั้นเป็นสิ่งที่ต้องพิจารณาเป็นพิเศษ ในความเป็นจริงบางครั้งมีการอ้างสิทธิ์ครั้งที่สามเนื่องจากคลาส polymorphic ถูกจัดขึ้นโดยตัวชี้คลาสพื้นฐานดังนั้นคุณจึงต้องยกเลิกการลงทะเบียนตัวชี้เพื่อรับที่อยู่ vtable (เพื่ออ้างอิงมัน ;-))
Nir Friedman

ในทางกลับกันความจริงที่ว่า vtable นั้นถูกเก็บไว้ภายนอกอินสแตนซ์นั้นจะมีประโยชน์สำหรับพื้นที่ชั่วคราวเทียบกับการพูด, กลุ่มของโครงสร้างที่แตกต่างกันของตัวชี้ฟังก์ชั่นที่ตัวชี้ฟังก์ชั่นแต่ละอัน ในกรณีเช่นนี้ vtable เดียวที่มีล้าน vptrs สามารถเอาชนะตัวชี้ฟังก์ชั่นนับล้านได้อย่างง่ายดาย (เริ่มต้นด้วยการใช้หน่วยความจำเพียงอย่างเดียว) มันอาจจะเป็นการโยนที่นี่ - ไม่ใช่เรื่องง่ายที่จะทำลายลง โดยทั่วไปแล้วฉันยอมรับว่าตัวชี้ฟังก์ชั่นมักจะถูกกว่าเล็กน้อย

ฉันคิดว่าใช้วิธีอื่นที่ฟังก์ชันเสมือนเริ่มต้นอย่างรวดเร็วและมีประสิทธิภาพสูงกว่าตัวชี้ฟังก์ชั่นตัวชี้คือเมื่อคุณมีจำนวนวัตถุของวัตถุที่เกี่ยวข้อง พอยน์เตอร์ของฟังก์ชั่นมักจะถูกกว่าถ้าคุณมีเพียงตัวชี้ฟังก์ชั่นเดียวที่เก็บไว้ในหน่วยความจำซึ่งจะเรียกว่า boatload ครั้ง มิฉะนั้นพอยน์เตอร์ของฟังก์ชั่นอาจเริ่มช้าลงด้วยปริมาณข้อมูลซ้ำซ้อนและแคชหายไปซึ่งเป็นผลมาจากหน่วยความจำที่ซ้ำซ้อนจำนวนมากและชี้ไปยังที่อยู่เดียวกัน

แน่นอนด้วยฟังก์ชั่นพอยน์เตอร์คุณยังสามารถเก็บไว้ในตำแหน่งศูนย์กลางแม้ว่าพวกเขาจะแชร์วัตถุแยกเป็นล้าน ๆ ชิ้นเพื่อหลีกเลี่ยงการเพิ่มหน่วยความจำและรับแคชที่ขาดหายไป แต่แล้วพวกเขาก็เริ่มที่จะเทียบเท่ากับ vpointers ที่เกี่ยวข้องกับการเข้าถึงตัวชี้ไปยังตำแหน่งที่ใช้ร่วมกันในหน่วยความจำเพื่อไปยังที่อยู่ฟังก์ชันจริงที่เราต้องการเรียก คำถามพื้นฐานที่นี่คือ: คุณเก็บที่อยู่ของฟังก์ชั่นไว้ใกล้กับข้อมูลที่คุณกำลังเข้าถึงหรืออยู่ในใจกลางเมืองมากขึ้นหรือไม่? vtables อนุญาตเฉพาะหลัง ตัวชี้ฟังก์ชั่นอนุญาตให้ทั้งสองวิธี

2

ฉันขออธิบายได้ไหมว่าทำไมฉันถึงคิดว่านี่เป็นปัญหา XY (คุณไม่ได้ถามตัวเองคนเดียว)

ฉันคิดว่าเป้าหมายที่แท้จริงของคุณคือการประหยัดเวลาโดยรวมไม่ใช่เพียงเพื่อทำความเข้าใจกับจุดที่เกี่ยวกับแคชคิดถึงและฟังก์ชั่นเสมือนจริง

นี่คือตัวอย่างของการปรับแต่งประสิทธิภาพจริงในซอฟต์แวร์จริง

ในซอฟต์แวร์จริงสิ่งต่าง ๆ ที่ทำขึ้นมาไม่ว่าโปรแกรมเมอร์จะมีประสบการณ์มากแค่ไหนก็สามารถทำได้ดีกว่า ไม่มีใครรู้ว่าพวกเขาคืออะไรจนกว่าจะมีการเขียนโปรแกรมและการปรับแต่งประสิทธิภาพสามารถทำได้ มีมากกว่าหนึ่งวิธีในการเพิ่มความเร็วของโปรแกรม ท้ายที่สุดการบอกว่าโปรแกรมนั้นเหมาะสมที่สุดคุณกำลังบอกว่าในส่วนของโปรแกรมที่เป็นไปได้ในการแก้ปัญหาของคุณไม่มีโปรแกรมใดที่ใช้เวลาน้อยลง จริงๆ?

ในตัวอย่างที่ฉันลิงก์ไปนั้นเดิมใช้เวลา 2700 ไมโครวินาทีต่อ "งาน" มีการแก้ไขปัญหาหกชุดซึ่งจะทวนเข็มนาฬิการอบ ๆ พิซซ่า การเร่งความเร็วครั้งแรกลบออก 33% ของเวลา อันที่สองลบ 11% แต่แจ้งให้ทราบล่วงหน้า, คนที่สองไม่ได้เป็น 11% ในช่วงเวลามันก็พบว่ามันเป็น 16% เนื่องจากปัญหาแรกก็หายไป ในทำนองเดียวกันปัญหาที่สามได้รับการขยายจาก 7.4% เป็น 13% (เกือบสองเท่า) เพราะปัญหาสองประการแรกหายไป

ในตอนท้ายกระบวนการขยายนี้อนุญาตให้ตัดออกได้ทั้งหมดยกเว้น 3.7 ไมโครวินาที นั่นคือ 0.14% ของเวลาเดิมหรือเพิ่มความเร็ว 730x

ป้อนคำอธิบายรูปภาพที่นี่

การลบปัญหาที่มีขนาดใหญ่ในตอนแรกให้ความเร็วพอสมควร แต่พวกเขาปูทางไปสู่การแก้ไขปัญหาในภายหลัง ปัญหาในภายหลังเหล่านี้อาจเป็นส่วนที่ไม่มีนัยสำคัญในตอนแรกของทั้งหมด แต่หลังจากปัญหาถูกลบออกในตอนต้นปัญหาเล็ก ๆ เหล่านี้มีขนาดใหญ่และสามารถสร้างความเร็วได้มาก (สิ่งสำคัญคือต้องเข้าใจว่าเพื่อให้ได้ผลลัพธ์นี้จะไม่มีใครพลาดและโพสต์นี้แสดงให้เห็นว่าสามารถทำได้ง่ายเพียงใด)

ป้อนคำอธิบายรูปภาพที่นี่

โปรแกรมสุดท้ายดีที่สุดหรือไม่? อาจจะไม่. ไม่มีการเร่งความเร็วใด ๆ ที่เกี่ยวข้องกับการพลาดแคช แคชจะคิดถึงตอนนี้หรือไม่ อาจจะ.

แก้ไข: ฉันได้รับ downvotes จากคนที่กลับบ้านใน "ส่วนที่สำคัญมาก" ของคำถามของ OP คุณไม่ทราบว่าบางสิ่งบางอย่าง "สำคัญมาก" จนกว่าคุณจะรู้ว่าเศษเสี้ยวของเวลาเป็นอย่างไร หากค่าเฉลี่ยของวิธีการเหล่านั้นถูกเรียกว่าเป็น 10 รอบขึ้นไปเมื่อเวลาผ่านไปวิธีการจัดส่งพวกเขาอาจจะไม่ "สำคัญ" เมื่อเทียบกับสิ่งที่พวกเขากำลังทำจริง ฉันเห็นสิ่งนี้ซ้ำแล้วซ้ำอีกซึ่งผู้คนปฏิบัติต่อ "ต้องการทุก ๆ เสี้ยววินาที" เป็นเหตุผลที่ทำให้เงินฉลาดและโง่เขลา


เขากล่าวแล้วว่าเขามี "ส่วนที่มีความสำคัญสูง" หลายอย่างที่ต้องการประสิทธิภาพการทำงานระดับนาโนวินาทีสุดท้าย ดังนั้นนี้ไม่ได้เป็นคำตอบให้กับคำถามที่เขาถาม (แม้ว่ามันจะเป็นคำตอบที่ดีให้กับคนอื่นคำถาม) ความ
gbjbaanb

2
@gbjbaanb: หากทุก ๆ เสี้ยววินาทีที่ผ่านมานับทำไมคำถามเริ่มต้นด้วย "โดยทั่วไป"? นั่นเป็นเรื่องไร้สาระ เมื่อ nanoseconds นับคุณจะไม่สามารถหาคำตอบทั่วไปได้คุณดูว่าคอมไพเลอร์ทำอะไรคุณดูว่าฮาร์ดแวร์ทำอะไรคุณลองใช้ชุดรูปแบบต่างๆและวัดทุกรูปแบบ
gnasher729

@ gnasher729 ฉันไม่รู้ แต่ทำไมมันถึงลงท้ายด้วย "ส่วนที่สำคัญมาก"? ฉันเดาเช่น slashdot เราควรอ่านเนื้อหาเสมอไม่ใช่เฉพาะชื่อ!
gbjbaanb

2
@gbjbaanb: ทุกคนบอกว่าพวกเขามี "ส่วนที่สำคัญมาก" พวกเขารู้ได้อย่างไร ฉันไม่รู้ว่ามีอะไรสำคัญจนกว่าฉันจะพูดพูดตัวอย่าง 10 ข้อและดูจาก 2 หรือมากกว่านั้น ในกรณีเช่นนี้หากวิธีการที่ถูกเรียกใช้คำสั่งมากกว่า 10 คำสั่งค่าใช้จ่ายของฟังก์ชั่นเสมือนอาจไม่มีนัยสำคัญ
Mike Dunlavey

@ gnasher729: อืมสิ่งแรกที่ฉันทำคือรับตัวอย่างสแต็คและแต่ละอันตรวจสอบสิ่งที่โปรแกรมกำลังทำอยู่และทำไม จากนั้นถ้ามันใช้เวลาทั้งหมดในใบไม้ของต้นไม้การโทรและการโทรทั้งหมดนั้นหลีกเลี่ยงไม่ได้อย่างแท้จริงมันไม่สำคัญว่าคอมไพเลอร์และฮาร์ดแวร์ทำอะไร คุณจะรู้ได้ว่าวิธีการจัดส่งเรื่องถ้าตัวอย่างที่ดินในกระบวนการทำวิธีการจัดส่ง
Mike Dunlavey
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.