Apakah memiliki satu fungsi virtual memperlambat seluruh kelas?
Atau hanya panggilan ke fungsi yang bersifat virtual? Dan apakah kecepatan akan terpengaruh jika fungsi virtual benar-benar ditimpa atau tidak, atau apakah ini tidak berpengaruh selama itu virtual.
Memiliki fungsi virtual memperlambat seluruh kelas sejauh satu item data lagi harus diinisialisasi, disalin,… saat berhadapan dengan objek dari kelas semacam itu. Untuk kelas dengan setengah lusin anggota atau lebih, perbedaannya harus diabaikan. Untuk kelas yang hanya berisi satu char
anggota, atau tanpa anggota sama sekali, perbedaannya mungkin terlihat.
Selain itu, penting untuk diperhatikan bahwa tidak setiap panggilan ke fungsi virtual adalah panggilan fungsi virtual. Jika Anda memiliki objek dengan tipe yang diketahui, kompilator dapat memancarkan kode untuk pemanggilan fungsi normal, dan bahkan dapat menyebariskan fungsi tersebut jika diinginkan. Hanya ketika Anda melakukan panggilan polimorfik, melalui pointer atau referensi yang mungkin menunjuk ke objek kelas dasar atau objek dari beberapa kelas turunan, Anda memerlukan tipuan vtable dan membayarnya dalam hal kinerja.
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
}
Langkah-langkah yang harus diambil perangkat keras pada dasarnya sama, tidak peduli apakah fungsinya ditimpa atau tidak. Alamat vtable dibaca dari objek, penunjuk fungsi diambil dari slot yang sesuai, dan fungsi dipanggil oleh penunjuk. Dalam hal kinerja aktual, prediksi cabang mungkin berdampak. Jadi misalnya, jika sebagian besar objek Anda merujuk ke implementasi yang sama dari fungsi virtual tertentu, maka ada beberapa kemungkinan bahwa prediktor cabang akan memprediksi dengan benar fungsi mana yang akan dipanggil bahkan sebelum pointer diambil. Tapi tidak masalah fungsi mana yang umum: bisa jadi sebagian besar objek didelegasikan ke kasus dasar yang tidak ditimpa, atau sebagian besar objek milik subkelas yang sama dan oleh karena itu mendelegasikan ke kasus yang ditimpa sama.
bagaimana penerapannya di tingkat yang dalam?
Saya suka ide jheriko untuk mendemonstrasikan ini menggunakan implementasi tiruan. Tetapi saya akan menggunakan C untuk mengimplementasikan sesuatu yang mirip dengan kode di atas, sehingga level rendah lebih mudah dilihat.
kelas orang tua 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!
}
kelas turunan Bar
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
}
fungsi f melakukan panggilan fungsi virtual
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);
}
Jadi Anda bisa lihat, vtable hanyalah blok statis dalam memori, sebagian besar berisi pointer fungsi. Setiap objek dari kelas polimorfik akan mengarah ke vtabel yang sesuai dengan tipe dinamisnya. Ini juga membuat koneksi antara RTTI dan fungsi virtual lebih jelas: Anda dapat memeriksa tipe kelas apa hanya dengan melihat apa yang ditunjuk oleh vtable. Hal di atas disederhanakan dalam banyak hal, seperti misalnya pewarisan ganda, tetapi konsep umumnya masuk akal.
Jika arg
bertipe Foo*
dan Anda ambil arg->vtable
, tetapi sebenarnya merupakan objek bertipe Bar
, maka Anda masih mendapatkan alamat yang benar dari vtable
. Itu karena vtable
selalu merupakan elemen pertama di alamat objek, tidak peduli apakah itu dipanggil vtable
atau base.vtable
dalam ekspresi yang diketik dengan benar.
Inside the C++ Object Model
denganStanley B. Lippman
. (Bagian 4.2, halaman 124-131)