ฟังก์ชันและประสิทธิภาพเสมือนจริง - C ++


125

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


ตามคำตอบของฉันฉันขอแนะนำให้ปิดสิ่งนี้เนื่องจากซ้ำกับstackoverflow.com/questions/113830
Suma


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

คำตอบ:


90

หลักการง่ายๆคือ:

ไม่ใช่ปัญหาด้านประสิทธิภาพจนกว่าคุณจะพิสูจน์ได้

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

บทความที่ดีที่พูดคุยเกี่ยวกับฟังก์ชั่นเสมือนจริง (และอื่น ๆ ) เป็นสมาชิกตัวชี้ฟังก์ชั่นและเร็วที่สุดที่เป็นไปได้ c ++ ผู้ได้รับมอบหมาย


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

2
@thomthom: ถูกต้องไม่มีความแตกต่างด้านประสิทธิภาพระหว่างฟังก์ชันเสมือนจริงและฟังก์ชันเสมือนธรรมดาทั่วไป
Greg Hewgill

168

คำถามของคุณทำให้ฉันสงสัยมากดังนั้นฉันจึงดำเนินการกำหนดเวลาบางอย่างบน CPU PowerPC ตามลำดับ 3GHz ที่เราใช้งานได้ การทดสอบที่ฉันใช้คือการสร้างคลาสเวกเตอร์ 4d อย่างง่ายพร้อมฟังก์ชัน get / set

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

จากนั้นฉันตั้งค่าอาร์เรย์สามอาร์เรย์โดยแต่ละอาร์เรย์มี 1024 เวกเตอร์เหล่านี้ (เล็กพอที่จะใส่ L1 ได้) และเรียกใช้ลูปที่เพิ่มเข้าด้วยกัน (Ax = Bx + Cx) 1,000 ครั้ง ฉันวิ่งนี้กับฟังก์ชั่นที่กำหนดไว้เป็นinline, virtualและฟังก์ชั่นการโทรปกติ นี่คือผลลัพธ์:

  • อินไลน์: 8ms (0.65ns ต่อการโทร)
  • โดยตรง: 68ms (5.53ns ต่อการโทร)
  • เสมือน: 160ms (13ns ต่อการโทร)

ดังนั้นในกรณีนี้ (ที่ทุกอย่างพอดีกับแคช) การเรียกฟังก์ชันเสมือนช้ากว่าการโทรแบบอินไลน์ประมาณ 20 เท่า แต่สิ่งนี้หมายความว่าอย่างไร? การเดินทางผ่านลูปแต่ละครั้งทำให้เกิด3 * 4 * 1024 = 12,288การเรียกใช้ฟังก์ชัน (เวกเตอร์ 1024 คูณสี่องค์ประกอบคูณสามการเรียกต่อการเพิ่ม) ดังนั้นเวลาเหล่านี้จึงแสดงถึง1000 * 12,288 = 12,288,000การเรียกใช้ฟังก์ชัน ลูปเสมือนใช้เวลานานกว่าลูปโดยตรง 92 มิลลิวินาทีดังนั้นค่าใช้จ่ายเพิ่มเติมต่อการโทรคือ 7 นาโนวินาทีต่อฟังก์ชัน

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

ดูเพิ่มเติม: การเปรียบเทียบชุดประกอบที่สร้างขึ้น


แต่ถ้าเรียกหลายครั้งก็มักจะถูกกว่าการโทรเพียงครั้งเดียว ดูบล็อกที่ไม่เกี่ยวข้องของฉัน: phresnel.org/blogโพสต์ที่ชื่อว่า "ฟังก์ชันเสมือนจริงถือว่าไม่เป็นอันตราย" แต่แน่นอนว่ามันขึ้นอยู่กับความซับซ้อนของ codepath ของคุณ
Sebastian Mach

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

10
นี่จะเป็นเกณฑ์มาตรฐานที่ดีสำหรับ gcc LTO (Link Time Optimization) ลองรวบรวมสิ่งนี้อีกครั้งโดยเปิดใช้ lto: gcc.gnu.org/wiki/LinkTimeOptimizationและดูว่าเกิดอะไรขึ้นกับปัจจัย 20x
lurscher

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

4
@thomthom ไม่ใช่ virtual / non-virtual เป็นแอตทริบิวต์ต่อฟังก์ชัน จำเป็นต้องกำหนดฟังก์ชันผ่าน vtable ก็ต่อเมื่อถูกทำเครื่องหมายเป็นเสมือนหรือถ้ามันแทนที่คลาสพื้นฐานที่มีเป็นเสมือน คุณมักจะเห็นคลาสที่มีกลุ่มของฟังก์ชันเสมือนสำหรับอินเทอร์เฟซสาธารณะจากนั้นก็มีตัวเข้าถึงอินไลน์จำนวนมากเป็นต้น (ในทางเทคนิคแล้วนี่เป็นการใช้งานโดยเฉพาะและคอมไพเลอร์สามารถใช้พอนเตอร์เสมือนได้แม้กระทั่งสำหรับฟังก์ชันที่มีเครื่องหมาย 'อินไลน์' แต่ผู้ที่เขียนคอมไพเลอร์ดังกล่าวจะเป็นบ้า)
Crashworks

42

เมื่อ Objective-C (โดยที่วิธีการทั้งหมดเป็นเสมือน) เป็นภาษาหลักสำหรับ iPhone และJavaของ freakin เป็นภาษาหลักสำหรับ Android ฉันคิดว่ามันค่อนข้างปลอดภัยที่จะใช้ฟังก์ชันเสมือน C ++ บนเสาดูอัลคอร์ 3 GHz ของเรา


4
ฉันไม่แน่ใจว่า iPhone เป็นตัวอย่างที่ดีของรหัสนักแสดง: youtube.com/watch?v=Pdk2cJpSXLg
Crashworks

13
@Crashworks: iPhone ไม่ใช่ตัวอย่างของโค้ดเลย เป็นตัวอย่างของฮาร์ดแวร์ - โดยเฉพาะฮาร์ดแวร์ที่ช้าซึ่งเป็นจุดที่ฉันทำที่นี่ หากภาษาที่ขึ้นชื่อว่า "ช้า" เหล่านี้ดีพอสำหรับฮาร์ดแวร์ที่ด้อยประสิทธิภาพฟังก์ชันเสมือนจริงจะไม่เป็นปัญหาใหญ่
Chuck

52
iPhone ทำงานบนโปรเซสเซอร์ ARM โปรเซสเซอร์ ARM ที่ใช้สำหรับ iOS ได้รับการออกแบบมาสำหรับ MHz ต่ำและใช้พลังงานต่ำ ไม่มีซิลิกอนสำหรับการทำนายสาขาบน CPU ดังนั้นจึงไม่มีค่าใช้จ่ายด้านประสิทธิภาพจากการทำนายสาขาที่พลาดจากการเรียกฟังก์ชันเสมือน นอกจากนี้ MHz สำหรับฮาร์ดแวร์ iOS ยังต่ำพอที่การพลาดแคชจะไม่ทำให้โปรเซสเซอร์หยุดทำงานเป็นเวลา 300 รอบนาฬิกาในขณะที่ดึงข้อมูลจาก RAM การพลาดแคชมีความสำคัญน้อยกว่าที่ MHz ต่ำกว่า กล่าวโดยสรุปไม่มีค่าใช้จ่ายใด ๆ จากการใช้ฟังก์ชันเสมือนบนอุปกรณ์ iOS แต่นี่เป็นปัญหาฮาร์ดแวร์และไม่มีผลกับซีพียูเดสก์ท็อป
HaltingState

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

@ AlexSuo ฉันไม่แน่ใจในจุดของคุณ? แน่นอนว่าการคอมไพล์ C ++ ไม่สามารถปรับให้เหมาะสมตามสิ่งที่อาจเกิดขึ้นที่รันไทม์ดังนั้นการคาดเดาและอื่น ๆ จะต้องดำเนินการโดย CPU เอง ... แต่คอมไพเลอร์ C ++ ที่ดี (หากได้รับคำแนะนำ) จะมีความยาวมากในการปรับฟังก์ชันให้เหมาะสมและลูปนานก่อน รันไทม์
underscore_d

34

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

การเรียกใช้ฟังก์ชันปกติสามารถสร้างแคชคำสั่งพลาดเมื่อ CPU ดึงคำสั่งแรกของฟังก์ชันใหม่และไม่ได้อยู่ในแคช

การเรียกใช้ฟังก์ชันเสมือนก่อนอื่นต้องโหลดตัวชี้ vtable จากออบเจ็กต์ ซึ่งอาจส่งผลให้พลาดแคชข้อมูล จากนั้นโหลดตัวชี้ฟังก์ชันจาก vtable ซึ่งอาจส่งผลให้แคชข้อมูลอื่นพลาด จากนั้นจะเรียกใช้ฟังก์ชันซึ่งอาจส่งผลให้แคชคำสั่งพลาดเหมือนฟังก์ชันที่ไม่ใช่ฟังก์ชันเสมือน

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


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

5
@Qwertie ฉันไม่คิดว่ามันจำเป็นจริงๆ เนื้อหาของลูป (ถ้ามีขนาดใหญ่กว่าแคช L1) สามารถ "เลิกใช้" ตัวชี้ vtable ตัวชี้ฟังก์ชันและการวนซ้ำในภายหลังจะต้องรอการเข้าถึงแคช L2 (หรือมากกว่า) ทุกครั้งที่ทำซ้ำ
Ghita

30

จากหน้า 44 ของคู่มือ "Optimizing Software in C ++"ของAgner Fog :

เวลาที่ใช้ในการเรียกฟังก์ชันสมาชิกเสมือนคือรอบนาฬิกาสองสามรอบมากกว่าที่จะเรียกฟังก์ชันสมาชิกที่ไม่ใช่เสมือนโดยที่คำสั่งเรียกฟังก์ชันจะเรียกฟังก์ชันเสมือนเวอร์ชันเดียวกันเสมอ หากเวอร์ชันมีการเปลี่ยนแปลงคุณจะได้รับการคาดคะเนผิด 10 - 30 รอบนาฬิกา กฎสำหรับการคาดคะเนและการคาดเดาผิดของการเรียกฟังก์ชันเสมือนเหมือนกับคำสั่ง switch ...


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

จากความทรงจำของฉันและการค้นหาอย่างรวดเร็ว - stackoverflow.com/questions/17061967/c-switch-and-jump-tables - ฉันสงสัยว่านี่เป็นจริงเสมอสำหรับswitch. แน่นอนว่าด้วยcaseค่าตามอำเภอใจโดยสิ้นเชิง แต่ถ้าทุกอย่างcaseติดต่อกันคอมไพเลอร์อาจจะปรับให้เหมาะสมลงในตารางการกระโดดได้ (อานั่นทำให้ฉันนึกถึง Z80 วันเก่าที่ดี) ซึ่งควรจะเป็นเวลาคงที่ (เพื่อต้องการคำที่ดีกว่า) ไม่ใช่ว่าฉันแนะนำให้ลองแทนที่ vfuncs switchซึ่งน่าหัวเราะ ;)
underscore_d

7

อย่างแน่นอน มันเป็นปัญหาในทางกลับกันเมื่อคอมพิวเตอร์ทำงานที่ 100Mhz เนื่องจากการเรียกเมธอดทุกครั้งต้องมีการค้นหาบน vtable ก่อนที่จะถูกเรียก แต่วันนี้ .. บนซีพียู 3Ghz ที่มีแคชระดับ 1 พร้อมหน่วยความจำมากกว่าคอมพิวเตอร์เครื่องแรกของฉัน? ไม่ใช่เลย. การจัดสรรหน่วยความจำจากแรมหลักจะทำให้คุณเสียเวลามากกว่าถ้าฟังก์ชันทั้งหมดของคุณเป็นเสมือนจริง

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

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

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


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

6
mp3 วันที่ตั้งแต่ปี 1995 ในปี 92 เราแทบจะไม่มี 386 ไม่มีทางที่พวกเขาจะเล่น mp3 ได้และ 50% ของเวลา cpu ถือว่าระบบปฏิบัติการหลายงานที่ดีกระบวนการไม่ได้ใช้งานและตัวกำหนดตารางเวลาล่วงหน้า ไม่มีสิ่งนี้ในตลาดผู้บริโภคในเวลานั้น มันเป็น 100% จากช่วงเวลาที่เปิดเครื่องตอนท้ายของเรื่อง
v.oddou

7

มีเกณฑ์ประสิทธิภาพอื่นนอกเหนือจากเวลาดำเนินการ Vtable ใช้พื้นที่หน่วยความจำเช่นกันและในบางกรณีสามารถหลีกเลี่ยงได้: ATL ใช้ " การเชื่อมโยงไดนามิกจำลอง " เวลาคอมไพล์กับเทมเพลตเพื่อให้ได้ผลของ "ความแตกต่างแบบคงที่" ซึ่งเป็นเรื่องยากที่จะอธิบาย โดยพื้นฐานแล้วคุณจะส่งคลาสที่ได้รับมาเป็นพารามิเตอร์ไปยังเทมเพลตคลาสพื้นฐานดังนั้นในเวลาคอมไพล์คลาสพื้นฐานจะ "รู้" ว่าคลาสที่ได้รับมาคืออะไรในแต่ละอินสแตนซ์ จะไม่อนุญาตให้คุณเก็บคลาสที่ได้รับมาหลายแบบในคอลเลกชันของประเภทพื้นฐาน (นั่นคือความหลากหลายแบบรันไทม์) แต่จากความรู้สึกคงที่ถ้าคุณต้องการสร้างคลาส Y ที่เหมือนกับคลาส X ที่มีอยู่ก่อนหน้าซึ่งมี hooks สำหรับการลบล้างประเภทนี้คุณเพียงแค่ต้องแทนที่วิธีการที่คุณสนใจจากนั้นคุณจะได้รับเมธอดพื้นฐานของคลาส X โดยไม่ต้องมี vtable

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

ดูคำถาม SO อื่น ๆ นี้ด้วย

นี่คือโพสต์ที่ฉันพบว่าพูดถึงด้านประสิทธิภาพเวลา CPU



4

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


1
บทความที่เชื่อมโยงไม่ถือว่าเป็นส่วนที่สำคัญมากของการโทรเสมือนจริงและอาจเป็นการคาดเดาผิดสาขาได้
สุมา

4

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

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


2
การเรียกอะไรก็ตามในวงที่แน่นมักจะทำให้รหัสและข้อมูลทั้งหมดนั้นร้อนในแคช ...
Greg Rogers

2
ใช่ แต่ถ้าการวนซ้ำทางขวานั้นวนซ้ำผ่านรายการของวัตถุแต่ละวัตถุอาจเรียกใช้ฟังก์ชันเสมือนในที่อยู่ที่แตกต่างกันผ่านการเรียกฟังก์ชันเดียวกัน
Daemin

3

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

นี่เป็นตัวอย่างที่ดีจากการทดสอบความแตกต่างของเวลา ~ 700% (!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

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

หรือเมื่อเป็นการโทรเสมือนที่ใช้ซ้ำ ๆ หลาย ๆ ครั้งในขณะที่ดำเนินการง่ายๆบางอย่าง - อาจเป็นเรื่องใหญ่มาก


4
++iaการเรียกฟังก์ชั่นเสมือนจริงที่มีราคาแพงเมื่อเทียบกับ แล้วไงล่ะ?
Bo Persson

2

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

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

นี่คือเอกสารฉบับหนึ่งที่วิเคราะห์แนวทางปฏิบัติที่ดีที่สุดสำหรับ C / C ++ ในบริบทระบบฝังตัว: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

สรุป: ขึ้นอยู่กับโปรแกรมเมอร์ที่จะเข้าใจข้อดี / ข้อเสียของการใช้โครงสร้างบางอย่างกับโครงสร้างอื่น หากคุณไม่ได้รับการขับเคลื่อนด้วยประสิทธิภาพขั้นสูงคุณอาจไม่สนใจเกี่ยวกับประสิทธิภาพที่ยอดเยี่ยมและควรใช้ OO ที่เป็นระเบียบทั้งหมดใน C ++ เพื่อช่วยให้โค้ดของคุณใช้งานได้มากที่สุด


2

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


1

สิ่งหนึ่งที่ควรทราบคือ:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

อาจเร็วกว่านี้:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

เนื่องจากวิธีแรกเรียกใช้ฟังก์ชันเดียวเท่านั้นในขณะที่วิธีที่สองอาจเรียกใช้ฟังก์ชันต่างๆมากมาย สิ่งนี้ใช้กับฟังก์ชันเสมือนจริงในภาษาใดก็ได้

ฉันพูดว่า "อาจ" เพราะขึ้นอยู่กับคอมไพเลอร์แคช ฯลฯ


0

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


2
ไม่เคยเลยไม่ว่าคอมพิวเตอร์เป้าหมายจะเล็กแค่ไหน?
zumalifeguard

ฉันอาจจะได้ตกลงที่มีคุณ phrased ว่าเป็นThe performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.ความแตกต่างที่สำคัญไม่ว่าจะเป็นไม่ได้sometimes never
underscore_d

-1

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

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

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

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


12
ฉันคิดว่าเป็นไปได้มากที่คอมไพเลอร์ของคุณสามารถบอกได้ว่าการเรียกฟังก์ชันเสมือนในโค้ดของคุณสามารถเรียก Virtual :: call เท่านั้น ในกรณีนั้นก็สามารถแทรกในบรรทัดได้ นอกจากนี้ยังไม่มีสิ่งใดที่ป้องกันไม่ให้คอมไพเลอร์แทรก Normal :: call แม้ว่าคุณจะไม่ได้ขอให้ก็ตาม ดังนั้นฉันคิดว่ามันค่อนข้างเป็นไปได้ที่คุณจะได้รับเวลาเท่ากันสำหรับการดำเนินการ 3 ครั้งเนื่องจากคอมไพเลอร์กำลังสร้างรหัสที่เหมือนกันสำหรับพวกเขา
Bjarke H. Roune
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.