ต้นทุนประสิทธิภาพของการมีเมธอดเสมือนในคลาส C ++ คืออะไร?


107

การมีเมธอดเสมือนอย่างน้อยหนึ่งวิธีในคลาส C ++ (หรือคลาสพาเรนต์ใด ๆ ) หมายความว่าคลาสนั้นจะมีตารางเสมือนและอินสแตนซ์ทุกตัวจะมีตัวชี้เสมือน

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

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

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



7
การเปรียบเทียบการโทรเสมือนกับไม่ใช่การโทรเสมือนไม่ใช่เรื่องจริง มีฟังก์ชันการทำงานที่แตกต่างกัน หากคุณต้องการเปรียบเทียบการเรียกฟังก์ชันเสมือนกับ C ที่เท่าเทียมกันคุณต้องเพิ่มต้นทุนของรหัสที่ใช้คุณสมบัติที่เทียบเท่ากันของฟังก์ชันเสมือน
Martin York

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


7
คำถามเกี่ยวกับการเรียกใช้ฟังก์ชันที่ไม่จำเป็นต้องเป็นเสมือนดังนั้นการเปรียบเทียบจึงมีความหมาย
Mark Ransom

คำตอบ:


104

ฉันรันการกำหนดเวลาบางอย่างบนตัวประมวลผล PowerPC ตามลำดับ 3GHz สำหรับสถาปัตยกรรมนั้นการเรียกใช้ฟังก์ชันเสมือนจะมีค่าใช้จ่ายนานกว่าการเรียกฟังก์ชันโดยตรง (ไม่ใช่เสมือน) ถึง 7 นาโนวินาที

ดังนั้นไม่ควรกังวลเกี่ยวกับค่าใช้จ่ายเว้นแต่ว่าฟังก์ชันนั้นจะเป็นตัวเข้าถึง Get () / Set () เล็กน้อยซึ่งสิ่งอื่นที่ไม่ใช่อินไลน์นั้นเป็นการสิ้นเปลือง ค่าใช้จ่าย 7ns ของฟังก์ชันที่อยู่ในแนว 0.5ns นั้นรุนแรง ค่าใช้จ่าย 7ns บนฟังก์ชันที่ใช้เวลา 500ms ในการดำเนินการนั้นไม่มีความหมาย

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

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

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

การควบคุมเวลาของฉันสำหรับอิทธิพลของ icache พลาดในการดำเนินการ (โดยเจตนาเนื่องจากฉันพยายามตรวจสอบท่อ CPU แบบแยก) ดังนั้นพวกเขาจึงลดต้นทุนนั้น


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

นานกว่า 7 นาโนวินาที หากการโทรปกติคือ 1 นาโนวินาทีซึ่งมีความสำคัญหากการโทรปกติคือ 70 นาโนวินาทีจะไม่เป็นเช่นนั้น
Martin York

หากคุณดูการกำหนดเวลาฉันพบว่าสำหรับฟังก์ชันที่มีราคา 0.66ns อินไลน์ค่าโสหุ้ยที่แตกต่างของการเรียกฟังก์ชันโดยตรงคือ 4.8ns และฟังก์ชันเสมือน 12.3ns (เทียบกับอินไลน์) คุณให้จุดที่ดีว่าถ้าฟังก์ชันนั้นมีราคาเป็นมิลลิวินาทีแล้ว 7 ns ก็ไม่มีความหมาย
Crashworks

2
มากกว่า 600 รอบ แต่ก็เป็นจุดที่ดี ฉันปล่อยมันออกจากกำหนดเวลาเพราะฉันสนใจแค่ค่าใช้จ่ายเนื่องจากฟองไปป์ไลน์และ prolog / epilog การพลาด icache เกิดขึ้นได้อย่างง่ายดายสำหรับการเรียกใช้ฟังก์ชันโดยตรง (Xenon ไม่มีตัวทำนายสาขา icache)
Crashworks

2
รายละเอียดเล็กน้อย แต่เกี่ยวกับ "อย่างไรก็ตามนี่ไม่ใช่ปัญหาเฉพาะสำหรับ ... " มันแย่กว่าเล็กน้อยสำหรับการจัดส่งเสมือนเนื่องจากมีหน้าพิเศษ (หรือสองหน้าถ้ามันตกข้ามขอบเขตของหน้า) ที่ต้องอยู่ในแคช - สำหรับ Virtual Dispatch Table ของคลาส
Tony Delroy

19

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

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

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

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

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


การเพิ่มประสิทธิภาพที่หายไปที่ใหญ่ที่สุดคือการอินไลน์โดยเฉพาะอย่างยิ่งถ้าฟังก์ชันเสมือนมักมีขนาดเล็กหรือว่างเปล่า
Zan Lynx

@ แอนดรูว์: มุมมองที่น่าสนใจ ฉันค่อนข้างไม่เห็นด้วยกับย่อหน้าสุดท้ายของคุณแม้ว่า: ถ้าคลาสพื้นฐานมีฟังก์ชันsaveที่อาศัยการใช้งานฟังก์ชันwriteในคลาสฐานสำหรับฉันแล้วดูเหมือนว่าsaveมีรหัสไม่ดีหรือwriteควรเป็นแบบส่วนตัว
MiniQuark

2
เพียงเพราะการเขียนเป็นแบบส่วนตัวไม่ได้ป้องกันไม่ให้ถูกลบล้าง นี่เป็นอีกหนึ่งข้อโต้แย้งที่ไม่ทำให้สิ่งต่าง ๆ เสมือนเป็นค่าเริ่มต้น ไม่ว่าในกรณีใดฉันกำลังคิดถึงสิ่งที่ตรงกันข้าม - การนำไปใช้งานทั่วไปและเป็นลายลักษณ์อักษรจะถูกแทนที่ด้วยสิ่งที่มีพฤติกรรมเฉพาะและไม่เข้ากันได้
Andrew Grant

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

และแผงขายของ icache อาจร้ายแรงมาก: 600 รอบในการทดสอบของฉัน
Crashworks

9

มันขึ้นอยู่กับ. :) (คุณคาดหวังอะไรอีกไหม?)

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

std :: copy () ในประเภท POD ธรรมดาสามารถใช้กิจวัตร memcpy แบบง่าย ๆ ได้ แต่ต้องจัดการประเภทที่ไม่ใช่ POD อย่างระมัดระวังมากขึ้น

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

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

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

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

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

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


7

หากคุณต้องการฟังก์ชั่นการจัดส่งเสมือนคุณต้องจ่ายราคา ข้อดีของ C ++ คือคุณสามารถใช้การใช้งาน virtual dispatch ที่จัดเตรียมโดยคอมไพลเลอร์ได้อย่างมีประสิทธิภาพแทนที่จะเป็นเวอร์ชันที่ไม่มีประสิทธิภาพที่คุณใช้งานเอง

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


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

5

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


การดำเนินการ

#include <iostream>
#include <vector>

// virtual dispatch model...

struct Base
{
    virtual int f() const { return 1; }
};

struct Derived : Base
{
    virtual int f() const { return 2; }
};

// alternative: member variable encodes runtime type...

struct Type
{
    Type(int type) : type_(type) { }
    int type_;
};

struct A : Type
{
    A() : Type(1) { }
    int f() const { return 1; }
};

struct B : Type
{
    B() : Type(2) { }
    int f() const { return 2; }
};

struct Timer
{
    Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
    struct timespec from;
    double elapsed() const
    {
        struct timespec to;
        clock_gettime(CLOCK_MONOTONIC, &to);
        return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
    }
};

int main(int argc)
{
  for (int j = 0; j < 3; ++j)
  {
    typedef std::vector<Base*> V;
    V v;

    for (int i = 0; i < 1000; ++i)
        v.push_back(i % 2 ? new Base : (Base*)new Derived);

    int total = 0;

    Timer tv;

    for (int i = 0; i < 100000; ++i)
        for (V::const_iterator i = v.begin(); i != v.end(); ++i)
            total += (*i)->f();

    double tve = tv.elapsed();

    std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';

    // ----------------------------

    typedef std::vector<Type*> W;
    W w;

    for (int i = 0; i < 1000; ++i)
        w.push_back(i % 2 ? (Type*)new A : (Type*)new B);

    total = 0;

    Timer tw;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
        {
            if ((*i)->type_ == 1)
                total += ((A*)(*i))->f();
            else
                total += ((B*)(*i))->f();
        }

    double twe = tw.elapsed();

    std::cout << "switched: " << total << ' ' << twe << '\n';

    // ----------------------------

    total = 0;

    Timer tw2;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
            total += (*i)->type_;

    double tw2e = tw2.elapsed();

    std::cout << "overheads: " << total << ' ' << tw2e << '\n';
  }
}

ผลการดำเนินงาน

บนระบบ Linux ของฉัน:

~/dev  g++ -O2 -o vdt vdt.cc -lrt
~/dev  ./vdt                     
virtual dispatch: 150000000 1.28025
switched: 150000000 0.344314
overhead: 150000000 0.229018
virtual dispatch: 150000000 1.285
switched: 150000000 0.345367
overhead: 150000000 0.231051
virtual dispatch: 150000000 1.28969
switched: 150000000 0.345876
overhead: 150000000 0.230726

สิ่งนี้ชี้ให้เห็นว่าวิธีการสลับประเภทตัวเลขแบบอินไลน์คือประมาณ (1.28 - 0.23) / (0.344 - 0.23) = เร็วขึ้น9.2เท่า แน่นอนว่าเป็นสิ่งที่เฉพาะเจาะจงสำหรับแฟล็กและเวอร์ชันที่ทดสอบ / คอมไพเลอร์ของระบบเท่านั้น แต่โดยทั่วไปจะบ่งชี้


ความคิดเห็น RE VIRTUAL DISPATCH

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


ฉันถามคำถามเกี่ยวกับรหัสของคุณเพราะฉันมีผลการค้นหา "แปลก" ใช้g++/ และclang -lrtฉันคิดว่ามันควรค่าแก่การกล่าวถึงที่นี่สำหรับผู้อ่านในอนาคต
Holt

@Holt: คำถามที่ดีให้ผลลัพธ์ที่น่าอัศจรรย์! ฉันจะให้มันดูใกล้ ๆ ในอีกไม่กี่วันถ้าฉันมีโอกาสสักครึ่ง ไชโย
Tony Delroy

3

ค่าใช้จ่ายเพิ่มเติมแทบไม่มีอะไรในสถานการณ์ส่วนใหญ่ (ให้อภัยเล่นสำนวน) ejac ได้โพสต์มาตรการสัมพัทธ์ที่สมเหตุสมผลแล้ว

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


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

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


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

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


2

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

พิจารณารหัสนี้ผลลัพธ์คืออะไร?

#include <stdio.h>

class A
{
public:
    void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

ไม่มีอะไรน่าแปลกใจที่นี่:

A::Foo()
B::Foo()
A::Foo()

ไม่มีอะไรเสมือนจริง หากคีย์เวิร์ดเสมือนถูกเพิ่มที่ด้านหน้าของ Foo ทั้งในคลาส A และ B เราจะได้รับสิ่งนี้สำหรับผลลัพธ์:

A::Foo()
B::Foo()
B::Foo()

เป็นสิ่งที่ทุกคนคาดหวัง

ตอนนี้คุณบอกว่ามีจุดบกพร่องเนื่องจากมีคนลืมใส่คำหลักเสมือน ดังนั้นให้พิจารณาโค้ดนี้ (โดยที่คีย์เวิร์ดเสมือนจะถูกเพิ่มลงในคลาส A แต่ไม่ใช่คลาส B) แล้วผลลัพธ์คืออะไร?

#include <stdio.h>

class A
{
public:
    virtual void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

คำตอบ: เช่นเดียวกับการเพิ่มคำหลักเสมือนลงใน B? เหตุผลก็คือลายเซ็นของ B :: Foo ตรงกับ A :: Foo () และเนื่องจาก A ของ Foo เป็นเสมือนจริงดังนั้นจึงเป็นของ B

ทีนี้ลองพิจารณากรณีที่ F ของ B เป็นเสมือนและ A ไม่ใช่ แล้วผลลัพธ์คืออะไร? ในกรณีนี้ผลลัพธ์คือ

A::Foo()
B::Foo()
A::Foo()

คีย์เวิร์ดเสมือนจะทำงานลงในลำดับชั้นไม่ใช่ขึ้นไป มันไม่เคยทำให้เมธอดคลาสพื้นฐานเสมือนจริง ครั้งแรกที่พบเมธอดเสมือนในลำดับชั้นคือเมื่อความหลากหลายเริ่มต้นขึ้น ไม่มีวิธีใดที่คลาสในภายหลังจะทำให้คลาสก่อนหน้ามีเมธอดเสมือนได้

อย่าลืมว่าวิธีการเสมือนหมายความว่าคลาสนี้กำลังให้คลาสในอนาคตสามารถลบล้าง / เปลี่ยนแปลงพฤติกรรมบางอย่างได้

ดังนั้นหากคุณมีกฎในการนำคีย์เวิร์ดเสมือนออกคำหลักนั้นอาจไม่มีผลตามที่ตั้งใจไว้

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


สวัสดีทอมมี่ขอบคุณสำหรับการสอน จุดบกพร่องที่เราพบเกิดจากคีย์เวิร์ด "เสมือน" ที่ขาดหายไปในเมธอดของคลาสพื้นฐาน BTW ฉันกำลังบอกว่าทำให้ฟังก์ชันทั้งหมดเสมือน (ไม่ใช่สิ่งที่ตรงกันข้าม) จากนั้นเมื่อไม่จำเป็นอย่างชัดเจนให้ลบคีย์เวิร์ด "เสมือน" ออก
MiniQuark

@MiniQuark: Tommy Hui กำลังบอกว่าถ้าคุณสร้างฟังก์ชันเสมือนจริงโปรแกรมเมอร์อาจจะลบคีย์เวิร์ดในคลาสที่ได้รับมาโดยไม่ทราบว่ามันไม่มีผลใด ๆ คุณต้องมีวิธีบางอย่างเพื่อให้แน่ใจว่าการลบคีย์เวิร์ดเสมือนจะเกิดขึ้นที่คลาสพื้นฐานเสมอ
M. Dudley

1

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

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


-1

มันจะต้องมีคำสั่ง asm พิเศษสองสามคำสั่งเพื่อเรียก virtual method

แต่ฉันไม่คิดว่าคุณจะกังวลว่า fun (int a, int b) มีคำสั่ง 'push' พิเศษสองสามคำเมื่อเทียบกับ fun () ดังนั้นอย่ากังวลกับเกมเสมือนจริงจนกว่าคุณจะอยู่ในสถานการณ์พิเศษและเห็นว่ามันนำไปสู่ปัญหาจริงๆ

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


เพื่อตอบสนองต่อความคิดเห็น "xtofl" และ "Tom" ฉันทำการทดสอบเล็ก ๆ ด้วย 3 ฟังก์ชั่น:

  1. เสมือน
  2. ปกติ
  3. ปกติที่มีพารามิเตอร์ int 3 ตัว

การทดสอบของฉันเป็นการทำซ้ำง่ายๆ:

for(int it = 0; it < 100000000; it ++) {
    test.Method();
}

และนี่คือผลลัพธ์:

  1. 3,913 วินาที
  2. 3,873 วินาที
  3. 3,970 วินาที

คอมไพล์โดย VC ++ ในโหมดดีบัก ฉันทำการทดสอบเพียง 5 ครั้งต่อวิธีและคำนวณค่าเฉลี่ย (ดังนั้นผลลัพธ์อาจไม่ถูกต้องนัก) ... อย่างไรก็ตามค่าเกือบจะเท่ากันโดยสมมติว่ามีการโทร 100 ล้านครั้ง และวิธีที่มี 3 push / pop พิเศษก็ช้าลง

ประเด็นหลักคือถ้าคุณไม่ชอบการเปรียบเทียบกับ push / pop ลองนึกถึง if / else ในโค้ดของคุณหรือไม่? คุณคิดเกี่ยวกับไปป์ไลน์ CPU หรือไม่เมื่อคุณเพิ่ม if / else ;-) นอกจากนี้คุณไม่มีทางรู้เลยว่า CPU ตัวใดที่โค้ดจะทำงาน ... คอมไพเลอร์ปกติสามารถสร้างโค้ดที่ดีที่สุดสำหรับซีพียูหนึ่งตัวและเหมาะสมกับอีกตัวหนึ่งน้อยกว่า ( Intel คอมไพเลอร์ C ++ )


2
asm พิเศษอาจทำให้เกิดข้อผิดพลาดของเพจ (ซึ่งจะไม่มีสำหรับฟังก์ชันที่ไม่ใช่เสมือนจริง) - ฉันคิดว่าคุณทำให้ปัญหาง่ายขึ้นอย่างมาก
xtofl

2
+1 ถึงความคิดเห็นของ xtofl ฟังก์ชันเสมือนนำเสนอทิศทางซึ่งแนะนำ "ฟอง" ไปป์ไลน์และส่งผลต่อพฤติกรรมการแคช
ทอม

1
การกำหนดเวลาในโหมดดีบักจะไม่มีความหมาย MSVC สร้างโค้ดที่ช้ามากในโหมดดีบักและโอเวอร์เฮดแบบวนซ้ำอาจซ่อนความแตกต่างส่วนใหญ่ หากคุณต้องการประสิทธิภาพสูงใช่คุณควรคิดถึงการลดสาขา if / else ในเส้นทางที่รวดเร็ว ดูagner.org/optimizeสำหรับข้อมูลเพิ่มเติมเกี่ยวกับการเพิ่มประสิทธิภาพ x86 ระดับต่ำ (รวมถึงลิงก์อื่น ๆ ในวิกิแท็ก x86
Peter Cordes

1
@Tom: ประเด็นสำคัญที่นี่คือฟังก์ชันที่ไม่ใช่เสมือนสามารถอินไลน์ได้ แต่เวอร์ชวลไม่สามารถทำได้ (เว้นแต่คอมไพลเลอร์จะเบี่ยงเบนไปได้เช่นถ้าคุณใช้finalในการแทนที่ของคุณและคุณมีตัวชี้ไปยังประเภทที่ได้รับแทนที่จะเป็นประเภทฐาน ). การทดสอบนี้เรียกว่าฟังก์ชันเสมือนจริงทุกครั้งดังนั้นจึงคาดการณ์ได้อย่างสมบูรณ์แบบ ไม่มีฟองสบู่อื่น ๆ ยกเว้นcallปริมาณงานที่จำกัด และทางอ้อมcallนั้นอาจเป็นอีกสองสามอุ๊บ การทำนายสาขาทำงานได้ดีแม้กระทั่งสำหรับสาขาทางอ้อมโดยเฉพาะอย่างยิ่งหากพวกเขาไปยังปลายทางเดียวกันเสมอ
Peter Cordes

สิ่งนี้ตกอยู่ในกับดักทั่วไปของ microbenchmarks: มันดูเร็วเมื่อตัวทำนายสาขาร้อนแรงและไม่มีอะไรเกิดขึ้นอีก ค่าใช้จ่ายใน mispredict เป็นที่สูงขึ้นสำหรับทางอ้อมกว่าโดยตรงcall call(และใช่callคำแนะนำปกติก็ต้องการการคาดเดาเช่นกันขั้นตอนการดึงข้อมูลต้องทราบที่อยู่ถัดไปที่จะดึงข้อมูลก่อนที่บล็อกนี้จะถูกถอดรหัสดังนั้นจึงต้องคาดเดาบล็อกการดึงข้อมูลถัดไปตามที่อยู่บล็อกปัจจุบันแทนที่จะเป็นที่อยู่คำสั่งเช่นกัน ตามที่คาดการณ์ว่าในบล็อกนี้มีคำสั่งสาขาใดบ้าง ... )
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.