การมีฟังก์ชันเสมือนเดียวทำให้ทั้งชั้นเรียนช้าลงหรือไม่?
หรือเฉพาะการเรียกใช้ฟังก์ชันที่เสมือนจริง? และความเร็วจะได้รับผลกระทบหรือไม่หากฟังก์ชันเสมือนถูกเขียนทับจริงหรือไม่หรือจะไม่มีผลตราบใดที่เป็นเสมือน
การมีฟังก์ชันเสมือนจะทำให้ทั้งคลาสทำงานช้าลงเนื่องจากต้องมีการเตรียมข้อมูลเบื้องต้นคัดลอก ... เมื่อจัดการกับอ็อบเจ็กต์ของคลาสดังกล่าว สำหรับชั้นเรียนที่มีสมาชิกประมาณครึ่งโหลความแตกต่างควรจะลบล้างได้ สำหรับชั้นเรียนที่มีchar
สมาชิกเพียงคนเดียวหรือไม่มีสมาชิกเลยความแตกต่างอาจมีความโดดเด่น
นอกเหนือจากนั้นสิ่งสำคัญคือต้องทราบว่าไม่ใช่ทุกการเรียกใช้ฟังก์ชันเสมือนเป็นการเรียกฟังก์ชันเสมือน หากคุณมีอ็อบเจ็กต์ประเภทที่รู้จักคอมไพลเลอร์สามารถปล่อยโค้ดสำหรับการเรียกใช้ฟังก์ชันปกติและยังสามารถอินไลน์ที่กล่าวว่าฟังก์ชันได้หากรู้สึกเช่นนั้น เฉพาะเมื่อคุณทำการเรียกแบบหลายรูปแบบผ่านตัวชี้หรือการอ้างอิงซึ่งอาจชี้ไปที่วัตถุของคลาสพื้นฐานหรือที่วัตถุของคลาสที่ได้รับบางอย่างเท่านั้นที่คุณต้องใช้ทิศทาง vtable และจ่ายในแง่ของประสิทธิภาพ
struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a(); // virtual: must dispatch via vtable
Foo z = arg; // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a(); // non-virtual Foo::a, since z is a Foo, even if arg was not
}
ขั้นตอนที่ฮาร์ดแวร์ต้องทำนั้นเหมือนกันหมดไม่ว่าฟังก์ชันนั้นจะถูกเขียนทับหรือไม่ก็ตาม ที่อยู่ของ vtable ถูกอ่านจากวัตถุตัวชี้ฟังก์ชันที่ดึงมาจากช่องที่เหมาะสมและฟังก์ชันที่เรียกโดยตัวชี้ ในแง่ของประสิทธิภาพจริงการคาดการณ์สาขาอาจมีผลกระทบบ้าง ตัวอย่างเช่นหากออบเจ็กต์ส่วนใหญ่ของคุณอ้างถึงการใช้งานฟังก์ชันเสมือนที่กำหนดแบบเดียวกันก็มีโอกาสที่ตัวทำนายสาขาจะคาดเดาได้อย่างถูกต้องว่าฟังก์ชันใดที่จะเรียกใช้ก่อนที่ตัวชี้จะถูกดึงออกมา แต่ไม่สำคัญว่าฟังก์ชันใดจะเป็นฟังก์ชันทั่วไป: อาจเป็นอ็อบเจ็กต์ส่วนใหญ่ที่มอบหมายให้กับเคสฐานที่ไม่ถูกเขียนทับหรืออ็อบเจ็กต์ส่วนใหญ่ที่อยู่ในคลาสย่อยเดียวกันดังนั้นจึงมอบหมายให้กับเคสที่เขียนทับเดียวกัน
การนำไปใช้ในระดับลึกเป็นอย่างไร
ฉันชอบความคิดของ jheriko ในการสาธิตสิ่งนี้โดยใช้การจำลองการใช้งาน แต่ฉันจะใช้ C เพื่อใช้งานบางอย่างที่คล้ายกับโค้ดด้านบนเพื่อให้มองเห็นระดับต่ำได้ง่ายขึ้น
ระดับผู้ปกครอง Foo
typedef struct Foo_t Foo; // forward declaration
struct slotsFoo { // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*); // virtual function Foo::a
};
struct Foo_t { // class Foo
const struct slotsFoo* vtable; // each instance points to vtable
};
void destructFoo(Foo* self) { } // Foo::~Foo
int aFoo(Foo* self) { return 1; } // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0, // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) { // Foo::Foo()
self->vtable = &vtableFoo; // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo; // don't copy from other!
}
คลาสบาร์ที่ได้รับ
typedef struct Bar_t { // class Bar
Foo base; // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { } // Bar::~Bar
int aBar(Bar* self) { return 2; } // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
&vtableFoo, // can dynamic_cast to Foo
(void(*)(Foo*)) destructBar, // must cast type to avoid errors
(int(*)(Foo*)) aBar
};
void constructBar(Bar* self) { // Bar::Bar()
self->base.vtable = &vtableBar; // point to Bar vtable
}
ฟังก์ชัน f ทำการเรียกฟังก์ชันเสมือน
void f(Foo* arg) { // same functionality as above
Foo x; constructFoo(&x); aFoo(&x);
Bar y; constructBar(&y); aBar(&y);
arg->vtable->a(arg); // virtual function call
Foo z; copyConstructFoo(&z, arg);
aFoo(&z);
destructFoo(&z);
destructBar(&y);
destructFoo(&x);
}
คุณจะเห็นได้ว่า vtable เป็นเพียงบล็อกแบบคงที่ในหน่วยความจำซึ่งส่วนใหญ่มีตัวชี้ฟังก์ชัน ทุกออบเจ็กต์ของคลาส polymorphic จะชี้ไปที่ vtable ที่สอดคล้องกับประเภทไดนามิก นอกจากนี้ยังทำให้การเชื่อมต่อระหว่าง RTTI และฟังก์ชันเสมือนชัดเจนขึ้น: คุณสามารถตรวจสอบประเภทของคลาสได้ง่ายๆเพียงแค่ดูว่า vtable นั้นชี้ไปที่อะไร ข้างต้นนั้นง่ายขึ้นในหลาย ๆ วิธีเช่นการสืบทอดหลาย ๆ อย่าง แต่แนวคิดทั่วไปนั้นฟังดูดี
หากarg
เป็นประเภทFoo*
และคุณใช้arg->vtable
แต่จริงๆแล้วเป็นวัตถุประเภทBar
คุณจะยังคงได้รับที่อยู่ที่ถูกต้องของไฟล์vtable
. นั่นเป็นเพราะvtable
องค์ประกอบแรกเป็นที่อยู่ของวัตถุเสมอไม่ว่าจะเรียกvtable
หรือbase.vtable
อยู่ในนิพจน์ที่พิมพ์ถูกต้องก็ตาม
Inside the C++ Object Model
โดยStanley B. Lippman
. (ข้อ 4.2 หน้า 124-131)